kenso 0.1.7 → 0.1.10

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/docs/reference.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Kenso CLI command reference
2
2
 
3
- Generated from kenso 0.1.7 (API contracts 54d399866b40) by `scripts/build-reference.ts`. Do not edit it: `pnpm build` writes it again.
3
+ Generated from kenso 0.1.10 (API contracts 3d429b7be4f5) by `scripts/build-reference.ts`. Do not edit it: `pnpm build` writes it again.
4
4
 
5
- 1358 commands. Beta commands work only on workspaces with their feature flag; `kenso describe` hides the ones yours cannot use. Every command also takes the global flags below. For the same data as JSON, run `kenso describe <resource> <action>`.
5
+ 1364 commands. Beta commands work only on workspaces with their feature flag; `kenso describe` hides the ones yours cannot use. Every command also takes the global flags below. For the same data as JSON, run `kenso describe <resource> <action>`.
6
6
 
7
7
  ## Global flags
8
8
 
@@ -76,6 +76,7 @@ Exit codes: 0 ok, 1 error, 2 usage, 3 auth, 4 not found, 5 some rows failed, 6 r
76
76
  - [bookings](#bookings) (5)
77
77
  - [brand-kit](#brand-kit) (3)
78
78
  - [brands](#brands) (1)
79
+ - [browser-runs](#browser-runs) (4)
79
80
  - [bug-reports](#bug-reports) (1)
80
81
  - [build-events](#build-events) (2)
81
82
  - [build-files](#build-files) (6)
@@ -244,7 +245,7 @@ Exit codes: 0 ok, 1 error, 2 usage, 3 auth, 4 not found, 5 some rows failed, 6 r
244
245
  - [meeting-results](#meeting-results) (7)
245
246
  - [meeting-shares](#meeting-shares) (3)
246
247
  - [meeting-types](#meeting-types) (6)
247
- - [meetings](#meetings) (24)
248
+ - [meetings](#meetings) (26)
248
249
  - [members](#members) (10)
249
250
  - [microsoft-teams-admin-consent-sessions](#microsoft-teams-admin-consent-sessions) (1)
250
251
  - [migration-connections](#migration-connections) (9)
@@ -489,7 +490,7 @@ kenso access-requests list --all
489
490
 
490
491
  Add a domain to an account. Returns the account.
491
492
 
492
- `domain`: a domain, website URL or work email (normalized to the registrable domain). `isPrimary` makes it the primary. A domain the account already has is a `200` no-op. One another account owns is `409 conflict` with `domain_taken` (and that `accountId`, unless account scope `own` hides that account). Requires `WriteAccess`.
493
+ `domain`: a domain, website URL or work email (normalized to the registrable domain). `isPrimary` makes it the primary. A domain the account already has is a `200` no-op. One another account owns is `409 conflict` with `domain_taken` (and that `accountId`, unless account scope `own` hides that account). Requires Accounts edit.
493
494
 
494
495
  - Beta: needs the `accounts` feature flag on the workspace.
495
496
  - API: `POST /v1/accounts/{accountId}/domains`
@@ -515,7 +516,7 @@ kenso account-domains create --account-id <account-id> --data @body.json
515
516
 
516
517
  Remove a domain from its account. Returns the account.
517
518
 
518
- The account's last domain cannot go (`409 invalid_state`, `last_domain`). Removing the primary promotes the oldest remaining domain. Requires `WriteAccess`.
519
+ The account's last domain cannot go (`409 invalid_state`, `last_domain`). Removing the primary promotes the oldest remaining domain. Requires Accounts edit.
519
520
 
520
521
  - Beta: needs the `accounts` feature flag on the workspace.
521
522
  - API: `DELETE /v1/account-domains/{accountDomainId}`
@@ -840,7 +841,7 @@ kenso account-tiers update --data @body.json
840
841
 
841
842
  Save a new account view at the end of the calling member's strip.
842
843
 
843
- Body: `name` (required, 1-80 characters after trim; names may repeat), `filters` (optional, the same `{ "field", "operator", "values" }` conditions as `accounts.search`; a condition with no values is dropped), `isActive` (optional; `true` makes the new view the active one and clears every view's draft). A member keeps at most 50 views, the four defaults included: the 51st is `409 conflict` with detail `view_limit`. Returns `201` with the view. Requires `ReadAccess`.
844
+ Body: `name` (required, 1-80 characters after trim; names may repeat), `filters` (optional, the same `{ "field", "operator", "values" }` conditions as `accounts.search`; a condition with no values is dropped), `isActive` (optional; `true` makes the new view the active one and clears every view's draft). A member keeps at most 50 views, the four defaults included: the 51st is `409 conflict` with detail `view_limit`. Returns `201` with the view. Requires Accounts view.
844
845
 
845
846
  - Beta: needs the `accounts` feature flag on the workspace.
846
847
  - API: `POST /v1/account-views`
@@ -865,7 +866,7 @@ kenso account-views create --data @body.json
865
866
 
866
867
  Delete one of the calling member's account views.
867
868
 
868
- Soft delete; returns `204`. Deleting the active view makes the default view active with no draft. The default view cannot be deleted (`409 invalid_state`, detail `default_view`). Later views move up one position. Requires `ReadAccess`.
869
+ Soft delete; returns `204`. Deleting the active view makes the default view active with no draft. The default view cannot be deleted (`409 invalid_state`, detail `default_view`). Later views move up one position. Requires Accounts view.
869
870
 
870
871
  - Beta: needs the `accounts` feature flag on the workspace.
871
872
  - API: `DELETE /v1/account-views/{accountViewId}`
@@ -885,7 +886,7 @@ kenso account-views delete --account-view-id <account-view-id>
885
886
 
886
887
  Copy one of the calling member's account views.
887
888
 
888
- Inserts `{name} copy` (a long name is cut so the copy fits in 80 characters) with the source's saved `filters` (not its draft) right after the source; later views move down one position. The copy becomes the active view, with every draft cleared, when the source was active. The default view cannot be duplicated (`409 invalid_state`, detail `default_view`); the 51st view is `409 conflict`, detail `view_limit`. Returns `201` with the copy. Requires `ReadAccess`.
889
+ Inserts `{name} copy` (a long name is cut so the copy fits in 80 characters) with the source's saved `filters` (not its draft) right after the source; later views move down one position. The copy becomes the active view, with every draft cleared, when the source was active. The default view cannot be duplicated (`409 invalid_state`, detail `default_view`); the 51st view is `409 conflict`, detail `view_limit`. Returns `201` with the copy. Requires Accounts view.
889
890
 
890
891
  - Beta: needs the `accounts` feature flag on the workspace.
891
892
  - API: `POST /v1/account-views/{accountViewId}/duplicate`
@@ -905,7 +906,7 @@ kenso account-views duplicate --account-view-id <account-view-id>
905
906
 
906
907
  List the calling member's saved account views.
907
908
 
908
- Every view in strip order (`position`, 0-based), at most 50, in one page (`pagination.hasMore` is always false). Exactly one view is `isActive`; only that view can carry `draftFilters` (unsaved edits, omitted when there are none). `accountCount` counts live accounts that pass the view's saved `filters`, not the draft and no `q` (with account scope `own`, only accounts the caller owns); for the active tab, show `pagination.total` of your own `accounts.search` (`includeTotal: true`) instead. The first call creates the four default views. `timeZone` (optional, an IANA zone) is the zone the lastTouch, latestSignal and nextTouch day buckets count calendar days in, as on `accounts.search`; the member's app time zone wins over it. Requires `ReadAccess`.
909
+ Every view in strip order (`position`, 0-based), at most 50, in one page (`pagination.hasMore` is always false). Exactly one view is `isActive`; only that view can carry `draftFilters` (unsaved edits, omitted when there are none). `accountCount` counts live accounts that pass the view's saved `filters`, not the draft and no `q` (with account scope `own`, only accounts the caller owns); for the active tab, show `pagination.total` of your own `accounts.search` (`includeTotal: true`) instead. The first call creates the four default views. `timeZone` (optional, an IANA zone) is the zone the lastTouch, latestSignal and nextTouch day buckets count calendar days in, as on `accounts.search`; the member's app time zone wins over it. Requires Accounts view.
909
910
 
910
911
  - Beta: needs the `accounts` feature flag on the workspace.
911
912
  - API: `GET /v1/account-views`
@@ -925,7 +926,7 @@ kenso account-views list
925
926
 
926
927
  Update one of the calling member's account views. Absent fields stay unchanged.
927
928
 
928
- <list type="bullet"> <item> `name`: 1-80 characters after trim. Cannot be null.</item> <item> `filters`: saves the view's conditions (null or `[]` = none) and clears its draft, unless `draftFilters` is in the same body ("Update view").</item> <item> `draftFilters`: unsaved conditions; null clears them ("Reset"). Only the view that is active after this request can hold a draft; a draft on any other view is `409 invalid_state` with detail `view_not_active`.</item> <item> `isActive`: only `true`, which makes this the active view and clears every other view's draft. `false` is `400`: activate another view instead.</item> </list> The default view (`isDefault`) cannot change `name` or `filters` (`409 invalid_state`, detail `default_view`); it can hold a draft ("Save as view" on All). Another member's or workspace's view is `404`. Requires `ReadAccess`.
929
+ <list type="bullet"> <item> `name`: 1-80 characters after trim. Cannot be null.</item> <item> `filters`: saves the view's conditions (null or `[]` = none) and clears its draft, unless `draftFilters` is in the same body ("Update view").</item> <item> `draftFilters`: unsaved conditions; null clears them ("Reset"). Only the view that is active after this request can hold a draft; a draft on any other view is `409 invalid_state` with detail `view_not_active`.</item> <item> `isActive`: only `true`, which makes this the active view and clears every other view's draft. `false` is `400`: activate another view instead.</item> </list> The default view (`isDefault`) cannot change `name` or `filters` (`409 invalid_state`, detail `default_view`); it can hold a draft ("Save as view" on All). Another member's or workspace's view is `404`. Requires Accounts view.
929
930
 
930
931
  - Beta: needs the `accounts` feature flag on the workspace.
931
932
  - API: `PATCH /v1/account-views/{accountViewId}`
@@ -1000,7 +1001,7 @@ kenso accounts add-to-campaign --account-id <account-id> --data @body.json
1000
1001
 
1001
1002
  Create up to 100 accounts in one call.
1002
1003
 
1003
- `items` (1 to 100) are each a `POST /v1/accounts` body. Returns 200 with one result per item in request order: `created` (with the account and `research`: queued or waiting_credits), `domain_taken` (the `accountId` that already owns `domain`; counted in skipped), `invalid` (`field`, `code` and `error` of the item's first problem) or `failed` (retry that item). The same domain twice in one call (acme.com and www.acme.com) creates once; the repeat is domain_taken. Duplicates are by domain only: acme.com and acme.io are two accounts. Ken resolves each company from Kenso Search and, when research is on, starts research per created account, charged its model tokens at 2x model cost, and each web page or search on Kenso's Firecrawl key at 2x Firecrawl cost; research waits for credits when the balance is short. With account scope `own`, every created account is the caller's, and an item naming another member's `ownerMemberId` is `invalid` with code `permission_denied`. Requires `WriteAccess`.
1004
+ `items` (1 to 100) are each a `POST /v1/accounts` body. Returns 200 with one result per item in request order: `created` (with the account and `research`: queued or waiting_credits), `domain_taken` (the `accountId` that already owns `domain`; counted in skipped), `invalid` (`field`, `code` and `error` of the item's first problem) or `failed` (retry that item). The same domain twice in one call (acme.com and www.acme.com) creates once; the repeat is domain_taken. Duplicates are by domain only: acme.com and acme.io are two accounts. Ken resolves each company from Kenso Search and, when research is on, starts research per created account, charged its model tokens at 2x model cost, and each web page or search on Kenso's Firecrawl key at 2x Firecrawl cost; research waits for credits when the balance is short. With account scope `own`, every created account is the caller's, and an item naming another member's `ownerMemberId` is `invalid` with code `permission_denied`. Requires Accounts edit.
1004
1005
 
1005
1006
  - Beta: needs the `accounts` feature flag on the workspace.
1006
1007
  - API: `POST /v1/accounts/bulk-create`
@@ -1045,7 +1046,7 @@ kenso accounts bulk-update --data @body.json
1045
1046
 
1046
1047
  Create an account from its domains.
1047
1048
 
1048
- `domains` (1 to 20 domains, website URLs or work emails; the first is primary) is required. A domain another account owns is `409 conflict` with detail `domain_taken` and that `accountId`: use that account instead. With account scope `own`, an account the caller cannot see is not named (the detail has no `accountId`), a missing, null or own `ownerMemberId` makes the caller the owner, and another member's is `403 permission_denied` with `scope: all`. Fields a person sets are pinned. Ken resolves the company from Kenso Search and fills the name (else from the primary domain: acme.com becomes Acme), industry, size, HQ and LinkedIn page without pinning them. When research is on, it starts research, charged its model tokens at 2x model cost, and each web page or search on Kenso's Firecrawl key at 2x Firecrawl cost. `skipResearch=true` creates the account without research or the committee run, so the create costs no credits; research can be started later with `POST /v1/accounts/{accountId}/research`. `sendsFromInboxId` and `sendsFromLinkedInAccountId` are checked like on PATCH. Requires `WriteAccess`.
1049
+ `domains` (1 to 20 domains, website URLs or work emails; the first is primary) is required. A domain another account owns is `409 conflict` with detail `domain_taken` and that `accountId`: use that account instead. With account scope `own`, an account the caller cannot see is not named (the detail has no `accountId`), a missing, null or own `ownerMemberId` makes the caller the owner, and another member's is `403 permission_denied` with `scope: all`. Fields a person sets are pinned. Ken resolves the company from Kenso Search and fills the name (else from the primary domain: acme.com becomes Acme), industry, size, HQ and LinkedIn page without pinning them. When research is on, it starts research, charged its model tokens at 2x model cost, and each web page or search on Kenso's Firecrawl key at 2x Firecrawl cost. `skipResearch=true` creates the account without research or the committee run, so the create costs no credits; research can be started later with `POST /v1/accounts/{accountId}/research`. `sendsFromInboxId` and `sendsFromLinkedInAccountId` are checked like on PATCH. Requires Accounts edit.
1049
1050
 
1050
1051
  - Beta: needs the `accounts` feature flag on the workspace.
1051
1052
  - API: `POST /v1/accounts`
@@ -1105,7 +1106,7 @@ kenso accounts delete --account-id <account-id>
1105
1106
 
1106
1107
  Count accounts per filter value for the filter panel.
1107
1108
 
1108
- Body: `filters`, the same conditions as `accounts.search` (optional). Returns `totalCount` (accounts that pass every filter) and one facet per field, in this order: `stage`, `committee`, `lastTouch`, `industry`, `fundingRound`, `headcount`, `signal`, `latestSignal`, `hasOpenOpportunity` (only while the opportunities feature is on), `tier`, `accountType`. Each value's `count` applies every other filter but not the field's own, so it is the number of accounts the list would show after adding that value (an account with several signal kinds counts under each). There is no `q`. Fixed fields list every value, zero counts included: stage in the order meeting_booked, engaged, warming, researching, nurture, lost, paused, not_a_fit; committee, lastTouch, headcount, signal, latestSignal and accountType in the order listed on `accounts.search`; tier 1 to the workspace ladder's tierCount. The facets take `filters` only, not `where`. `industry` and `fundingRound` list the 100 values most used across every account in the workspace, zero counts included. Industry sorts by count, highest first, then name; funding round puts Pre-seed, Seed and Series A to E first, then the rest alphabetically. With account scope `own`, the counts, `totalCount` and the industry and funding round values come only from the accounts the caller owns. Requires `ReadAccess`.
1109
+ Body: `filters`, the same conditions as `accounts.search` (optional). Returns `totalCount` (accounts that pass every filter) and one facet per field, in this order: `stage`, `committee`, `lastTouch`, `industry`, `fundingRound`, `headcount`, `signal`, `latestSignal`, `hasOpenOpportunity` (only while the opportunities feature is on), `tier`, `accountType`. Each value's `count` applies every other filter but not the field's own, so it is the number of accounts the list would show after adding that value (an account with several signal kinds counts under each). There is no `q`. Fixed fields list every value, zero counts included: stage in the order meeting_booked, engaged, warming, researching, nurture, lost, paused, not_a_fit; committee, lastTouch, headcount, signal, latestSignal and accountType in the order listed on `accounts.search`; tier 1 to the workspace ladder's tierCount. The facets take `filters` only, not `where`. `industry` and `fundingRound` list the 100 values most used across every account in the workspace, zero counts included. Industry sorts by count, highest first, then name; funding round puts Pre-seed, Seed and Series A to E first, then the rest alphabetically. With account scope `own`, the counts, `totalCount` and the industry and funding round values come only from the accounts the caller owns. Requires Accounts view.
1109
1110
 
1110
1111
  - Beta: needs the `accounts` feature flag on the workspace.
1111
1112
  - API: `POST /v1/accounts/facets`
@@ -1128,7 +1129,7 @@ kenso accounts facets --data @body.json
1128
1129
 
1129
1130
  Return one account.
1130
1131
 
1131
- `expand=stakeholders` embeds up to 200 stakeholders with their person. `expand=research` embeds the research dossier (status, trigger, errorCode, researchedAt, isStale, whyNow, angle, fitScore, tagline, description, facts, links, pains, stack, signals, sources). The status is the latest run's; the dossier fields come from the latest succeeded run, so a refresh in progress still shows the previous dossier. researchStatus (none | waiting_credits | queued | running | succeeded | failed) and researchedAt are on every account in get, list and search. `score` (0-99 from the signals of the last 90 days and the engaged people), `scoredAt`, `signalCount`, `engagedPeopleCount` and `latestSignal` (id, kind, title, occurredAt, detectedAt) are absent until the account is first scored. `handoff` is the latest hand-off to a teammate (`accounts.hand-off`), omitted when there is none. Requires `ReadAccess`.
1132
+ `expand=stakeholders` embeds up to 200 stakeholders with their person. `expand=research` embeds the research dossier (status, trigger, errorCode, researchedAt, isStale, whyNow, angle, fitScore, tagline, description, facts, links, pains, stack, signals, sources). The status is the latest run's; the dossier fields come from the latest succeeded run, so a refresh in progress still shows the previous dossier. researchStatus (none | waiting_credits | queued | running | succeeded | failed) and researchedAt are on every account in get, list and search. `score` (0-99 from the signals of the last 90 days and the engaged people), `scoredAt`, `signalCount`, `engagedPeopleCount` and `latestSignal` (id, kind, title, occurredAt, detectedAt) are absent until the account is first scored. `handoff` is the latest hand-off to a teammate (`accounts.hand-off`), omitted when there is none. Requires Accounts view.
1132
1133
 
1133
1134
  - Beta: needs the `accounts` feature flag on the workspace.
1134
1135
  - API: `GET /v1/accounts/{accountId}`
@@ -1261,7 +1262,7 @@ kenso accounts links --account-id <account-id>
1261
1262
 
1262
1263
  List accounts in the current workspace.
1263
1264
 
1264
- Filters: `stage` (comma = OR: researching, warming, engaged, meeting_booked, nurture, lost, paused, not_a_fit), `ownerMemberId`, `domain` (any domain, URL or email; normalized), `q` (name or domain contains), `nextTouchBefore`, `latestSignalAfter` (the latest signal's event time is at or after it), `tagIds` (comma = OR: accounts carrying any of these tags; up to 100 positive ids, anything else is `400 validation_failed`), `hasOpenOpportunity` (`true`: accounts with an open opportunity, `false`: accounts without), `isCustomer` (`true`: customer accounts, `false`: the rest), `accountType` (comma = OR: prospect, client, lost, churned, partner, competitor, investor, do_not_contact), `fitsIcp` (`true`: accounts that fit the ICP, which are the accounts Today surfaces for their signals: none of the account's domains is one of the workspace's own domains (Settings > Company domains and the Primary), its stage is not `not_a_fit`, and research has not scored its fit below 20 (an account not researched yet fits); `false`: the accounts that do not). `sort`: `-createdAt` (default), `name`, `nextTouchAt`, `-score` (highest first, never scored last), `-latestSignalAt` (newest first, no signal last). Today's Heating up is `latestSignalAfter=<now - 7 days>&fitsIcp=true&sort=-score&limit=4`. Each item embeds its domains, its `tagIds` and, when it has one, its `latestSignal`, and carries `openOpportunityCount`, `openPipelineAmount` and `openOpportunityStage`, and, once handed off, its latest `handoff`. `score` (0-99), `scoredAt`, `signalCount` and `engagedPeopleCount` are absent until the account is first scored. `expand=committee` adds each account's `committee`: `stakeholderCount`, `openSeatCount`, `coverage` (covered and total, as on `GET /v1/accounts/{accountId}/committee`), `roles` (role, stakeholderCount and openSeatCount per role that has either) and `run`, the latest mapping run as the committee read returns it (omitted when Ken never mapped the account). Without `expand` there is no `committee`. Requires `ReadAccess`.
1265
+ Filters: `stage` (comma = OR: researching, warming, engaged, meeting_booked, nurture, lost, paused, not_a_fit), `ownerMemberId`, `domain` (any domain, URL or email; normalized), `q` (name or domain contains), `nextTouchBefore`, `latestSignalAfter` (the latest signal's event time is at or after it), `tagIds` (comma = OR: accounts carrying any of these tags; up to 100 positive ids, anything else is `400 validation_failed`), `hasOpenOpportunity` (`true`: accounts with an open opportunity, `false`: accounts without), `isCustomer` (`true`: customer accounts, `false`: the rest), `accountType` (comma = OR: prospect, client, lost, churned, partner, competitor, investor, do_not_contact), `fitsIcp` (`true`: accounts that fit the ICP, which are the accounts Today surfaces for their signals: none of the account's domains is one of the workspace's own domains (Settings > Company domains and the Primary), its stage is not `not_a_fit`, and research has not scored its fit below 20 (an account not researched yet fits); `false`: the accounts that do not). `sort`: `-createdAt` (default), `name`, `nextTouchAt`, `-score` (highest first, never scored last), `-latestSignalAt` (newest first, no signal last). Today's Heating up is `latestSignalAfter=<now - 7 days>&fitsIcp=true&sort=-score&limit=4`. Each item embeds its domains, its `tagIds` and, when it has one, its `latestSignal`, and carries `openOpportunityCount`, `openPipelineAmount` and `openOpportunityStage`, and, once handed off, its latest `handoff`. `score` (0-99), `scoredAt`, `signalCount` and `engagedPeopleCount` are absent until the account is first scored. `expand=committee` adds each account's `committee`: `stakeholderCount`, `openSeatCount`, `coverage` (covered and total, as on `GET /v1/accounts/{accountId}/committee`), `roles` (role, stakeholderCount and openSeatCount per role that has either) and `run`, the latest mapping run as the committee read returns it (omitted when Ken never mapped the account). Without `expand` there is no `committee`. Requires Accounts view.
1265
1266
 
1266
1267
  - Beta: needs the `accounts` feature flag on the workspace.
1267
1268
  - API: `GET /v1/accounts` (a list: `--limit`, `--cursor`, `--all`)
@@ -1349,7 +1350,7 @@ kenso accounts research --account-id <account-id> --data @body.json
1349
1350
 
1350
1351
  Search accounts in the current workspace with filter conditions and AND / OR groups.
1351
1352
 
1352
- Body: `filters`, `where`, `q`, `sort`, `limit` (1-200, default 50), `cursor`, `includeTotal`. Every field is optional; an empty body lists every account. `filters`, `where` and `q` AND together. `filters` is a list of `{ "field", "operator", "values" }` conditions. Conditions AND together; the values of one condition OR together. `operator` is `is` or `is_not`; `is_not` also matches an account that has no value for the field. One condition per field (a second is `400 validation_failed`, detail `duplicate_field`). 1 to 50 values per condition, duplicates removed; a condition with an empty `values` list is ignored. Fields and values:<list type="bullet"><item>`stage`: researching, warming, engaged, meeting_booked, nurture, lost, paused, not_a_fit.</item><item>`committee`: n = stakeholders with coverage engaged or planned, t = all stakeholders. `none_engaged` when n = 0 (also with no stakeholders), `under_half` when n / t < 0.5, else `half_or_more`.</item><item>`lastTouch`: `past_7_days` (lastTouchAt within 7 days), `one_to_four_weeks` (7 to 28 days ago), `over_a_month` (older), `never` (no touch sent or done).</item><item>`headcount`: `under_250`, `from_250_to_999`, `from_1000` (employeeCount). An account with no employeeCount is in no bucket.</item><item>`industry`, `fundingRound`: any text, 1-200 characters, exact match. Read the values in use from `accounts.facets`.</item><item>`signal`: job_change, funding, hiring, public_post, event, asset_view, tech_change. An account matches a value when it has a signal of that kind whose event time (when it happened, else when Ken found it) is in the last 90 days; `job_change` also matches `leadership_hire`. `is_not` matches an account with none of them.</item><item>`latestSignal`: the latest signal's event time in the `lastTouch` buckets: `past_7_days`, `one_to_four_weeks`, `over_a_month`, `never` (no signal, or not scored yet).</item><item>`hasOpenOpportunity`: `true` (the account has a live opportunity in an open stage) or `false`. It is also a facet field with yes and no counts.</item><item>`isCustomer`: `true` (the account is marked a customer) or `false`. Not a facet field.</item><item>`tier`: 1 to 10, the tier the account is worked at (the hand-set tier, else the placed tier, else the tier its score falls in on the workspace's ladder). A tier past the ladder's tierCount matches no account. It is also a facet field.</item><item>`accountType`: prospect, client, lost, churned, partner, competitor, investor, do_not_contact. It is also a facet field.</item></list> `where` is a group `{ "match": "all" | "any", "conditions": [...] }`: `all` needs every entry to hold, `any` at least one. An entry is a condition `{ "field", "operator", "values" }` or a group, one level deep (a group inside a group inside `where` is `400`), with at most 50 conditions in all. A field may appear in several conditions. A condition whose operator needs values and that has none is dropped, and a group with no entries matches every account. Every `filters` field takes `is` and `is_not` with the values above, and where adds:<list type="bullet"><item>`headcount``lt` / `gte`: one whole number of employees (0 to 10,000,000). An account with no employeeCount matches neither.</item><item>`committee``lt` / `gte`: one share from 0 to 100, 100 n / t (no committee is 0).</item><item>`lastTouch`, `latestSignal``within_days` / `older_than_days`: one whole number of days from 1 to 365. Within: in the last N days. Older: more than N days ago; an account never touched (or with no signal) matches neither.</item><item>`owner`: `is` / `is_not` member ids (`is_not` also matches no owner), or `is_empty` (no owner, no values).</item><item>`tags`: tag ids. `is`: the account has any of them; `is_not`: it has none of them.</item><item>`domain`: `is` (a domain, URL or work email, normalized like a domain write; a free email provider is `400`), `contains` or `ends_with` (a lower-case piece such as `acme` or `.io`). It matches when any of the account's domains does.</item><item>`pipelineStage`: the stage key of the account's open opportunities. `is`: one of them is in one of these stages; `is_not`: none is; `is_empty`: the account has no open opportunity.</item><item>`nextTouch`: `is_planned`, `is_not_planned` (no values), or `within_days` N (planned for the next N days, 1 to 365; an overdue touch counts).</item></list> Example: <c>{"where": {"match": "any", "conditions": [{"field": "tier", "operator": "is", "values": ["1"]}, {"match": "all", "conditions": [{"field": "accountType", "operator": "is", "values": ["prospect"]}, {"field": "lastTouch", "operator": "older_than_days", "values": ["30"]}]}]}}</c>: tier 1 accounts, and prospects last touched more than 30 days ago. Every problem in `filters` or `where` is listed in one `400 validation_failed`, each with its path (`where.conditions[1].conditions[0].values[0]`). `q` (name or domain contains) and `sort` (`-createdAt` default, `name`, `nextTouchAt`, `-score`, `-latestSignalAt`) work as on `accounts.list`. `includeTotal: true` fills `pagination.total` with every account that passes the filters, where and q. Each item embeds its domains. Requires `ReadAccess`. Days: with a time zone, `lastTouch`, `latestSignal` and `nextTouch` count calendar days in it, as the app shows them. The zone is the member's app time zone (Settings), else `timeZone` (an IANA zone such as `Europe/Bucharest`; an unknown one is `400`). Past 7 days is today and the 7 days before it (from the zone's midnight 7 days ago), 1-4 weeks back to the zone's midnight 28 days ago, `within_days` N from the zone's midnight N days ago (`older_than_days` is before it), and `nextTouch``within_days` N before the zone's midnight after the day N days ahead. With no zone the windows roll back from now.
1353
+ Body: `filters`, `where`, `q`, `sort`, `limit` (1-200, default 50), `cursor`, `includeTotal`. Every field is optional; an empty body lists every account. `filters`, `where` and `q` AND together. `filters` is a list of `{ "field", "operator", "values" }` conditions. Conditions AND together; the values of one condition OR together. `operator` is `is` or `is_not`; `is_not` also matches an account that has no value for the field. One condition per field (a second is `400 validation_failed`, detail `duplicate_field`). 1 to 50 values per condition, duplicates removed; a condition with an empty `values` list is ignored. Fields and values:<list type="bullet"><item>`stage`: researching, warming, engaged, meeting_booked, nurture, lost, paused, not_a_fit.</item><item>`committee`: n = stakeholders with coverage engaged or planned, t = all stakeholders. `none_engaged` when n = 0 (also with no stakeholders), `under_half` when n / t < 0.5, else `half_or_more`.</item><item>`lastTouch`: `past_7_days` (lastTouchAt within 7 days), `one_to_four_weeks` (7 to 28 days ago), `over_a_month` (older), `never` (no touch sent or done).</item><item>`headcount`: `under_250`, `from_250_to_999`, `from_1000` (employeeCount). An account with no employeeCount is in no bucket.</item><item>`industry`, `fundingRound`: any text, 1-200 characters, exact match. Read the values in use from `accounts.facets`.</item><item>`signal`: job_change, funding, hiring, public_post, event, asset_view, tech_change. An account matches a value when it has a signal of that kind whose event time (when it happened, else when Ken found it) is in the last 90 days; `job_change` also matches `leadership_hire`. `is_not` matches an account with none of them.</item><item>`latestSignal`: the latest signal's event time in the `lastTouch` buckets: `past_7_days`, `one_to_four_weeks`, `over_a_month`, `never` (no signal, or not scored yet).</item><item>`hasOpenOpportunity`: `true` (the account has a live opportunity in an open stage) or `false`. It is also a facet field with yes and no counts.</item><item>`isCustomer`: `true` (the account is marked a customer) or `false`. Not a facet field.</item><item>`tier`: 1 to 10, the tier the account is worked at (the hand-set tier, else the placed tier, else the tier its score falls in on the workspace's ladder). A tier past the ladder's tierCount matches no account. It is also a facet field.</item><item>`accountType`: prospect, client, lost, churned, partner, competitor, investor, do_not_contact. It is also a facet field.</item></list> `where` is a group `{ "match": "all" | "any", "conditions": [...] }`: `all` needs every entry to hold, `any` at least one. An entry is a condition `{ "field", "operator", "values" }` or a group, one level deep (a group inside a group inside `where` is `400`), with at most 50 conditions in all. A field may appear in several conditions. A condition whose operator needs values and that has none is dropped, and a group with no entries matches every account. Every `filters` field takes `is` and `is_not` with the values above, and where adds:<list type="bullet"><item>`headcount``lt` / `gte`: one whole number of employees (0 to 10,000,000). An account with no employeeCount matches neither.</item><item>`committee``lt` / `gte`: one share from 0 to 100, 100 n / t (no committee is 0).</item><item>`lastTouch`, `latestSignal``within_days` / `older_than_days`: one whole number of days from 1 to 365. Within: in the last N days. Older: more than N days ago; an account never touched (or with no signal) matches neither.</item><item>`owner`: `is` / `is_not` member ids (`is_not` also matches no owner), or `is_empty` (no owner, no values).</item><item>`tags`: tag ids. `is`: the account has any of them; `is_not`: it has none of them.</item><item>`domain`: `is` (a domain, URL or work email, normalized like a domain write; a free email provider is `400`), `contains` or `ends_with` (a lower-case piece such as `acme` or `.io`). It matches when any of the account's domains does.</item><item>`pipelineStage`: the stage key of the account's open opportunities. `is`: one of them is in one of these stages; `is_not`: none is; `is_empty`: the account has no open opportunity.</item><item>`nextTouch`: `is_planned`, `is_not_planned` (no values), or `within_days` N (planned for the next N days, 1 to 365; an overdue touch counts).</item></list> Example: <c>{"where": {"match": "any", "conditions": [{"field": "tier", "operator": "is", "values": ["1"]}, {"match": "all", "conditions": [{"field": "accountType", "operator": "is", "values": ["prospect"]}, {"field": "lastTouch", "operator": "older_than_days", "values": ["30"]}]}]}}</c>: tier 1 accounts, and prospects last touched more than 30 days ago. Every problem in `filters` or `where` is listed in one `400 validation_failed`, each with its path (`where.conditions[1].conditions[0].values[0]`). `q` (name or domain contains) and `sort` (`-createdAt` default, `name`, `nextTouchAt`, `-score`, `-latestSignalAt`) work as on `accounts.list`. `includeTotal: true` fills `pagination.total` with every account that passes the filters, where and q. Each item embeds its domains. Requires Accounts view. Days: with a time zone, `lastTouch`, `latestSignal` and `nextTouch` count calendar days in it, as the app shows them. The zone is the member's app time zone (Settings), else `timeZone` (an IANA zone such as `Europe/Bucharest`; an unknown one is `400`). Past 7 days is today and the 7 days before it (from the zone's midnight 7 days ago), 1-4 weeks back to the zone's midnight 28 days ago, `within_days` N from the zone's midnight N days ago (`older_than_days` is before it), and `nextTouch``within_days` N before the zone's midnight after the day N days ahead. With no zone the windows roll back from now.
1353
1354
 
1354
1355
  - Beta: needs the `accounts` feature flag on the workspace.
1355
1356
  - API: `POST /v1/accounts/search`
@@ -1380,7 +1381,7 @@ kenso accounts search --data @body.json
1380
1381
 
1381
1382
  Update account fields. Absent fields stay unchanged; null clears.
1382
1383
 
1383
- Fields: name, stage, ownerMemberId (a workspace member), fitScore (0-100), whyNow, angle, cadenceDays (1-365), industry, fundingRound (1-100 characters), employeeCount, hqLocation, linkedinUrl, kenCompanyId (the company's Kenso id), sendsFromInboxId, sendsFromLinkedInAccountId. `sendsFromInboxId` is the inbox email touches send from: a member's primary inbox (owned and on the workspace's primary domain), or a shared campaign inbox on one of the workspace's cold sending domains, whose touches the account owner approves. `sendsFromLinkedInAccountId` is the LinkedIn account LinkedIn touches send from, connected by a member. Null on either means the owner's default. Neither is pinned, so an agent may change them. A bad id is `400 validation_failed` with code `not_found`, `not_owned`, `not_member` (its member left the workspace) or `email_domain_mismatch` (the inbox is not on the workspace's email domain; the message says what fixes it). Only a changed id is checked, so a form that sends the stored ids back saves. A disconnected sender is accepted; its touches wait until it reconnects. Changing a field a person set is `409 conflict` with `pinned_by_user` unless `overwritePinned: true`; nothing is applied partially. With account scope `own`, an account the caller does not own is `404 not_found`, and an `ownerMemberId` other than the caller's own is `403 permission_denied` with `scope: all`. Requires `WriteAccess`. `tierOverride` (1 to the ladder's `tierCount`; a higher tier is `400 validation_failed`) sets the account's tier by hand after the other fields are saved, and routes it to that tier's destination at once where the workspace has tier routing on (account-tiers); null is Auto: the account goes back to its auto tier (the tier the ladder placed it at, else its score's tier) without routing, and the ladder moves it from then on. Both write a tier change to the account's activity, with tier routing on or off. A tier whose move adds the account's people to a campaign, or stops the people the ladder put in one, also requires campaigns edit (`403 permission_denied`); both are checked before anything is saved. The response's `tier` is the hand-set tier, else `autoTier`. When another tier change on the account ran at the same time, the tier is not set and the response is `409 conflict`; the other fields are saved. A campaign that refuses the people (the plan's lead limit) also leaves the tier unset, with the other fields saved. `isCustomer` (true or false) marks the account a customer or clears it, after the other fields are saved. Only a workspace admin can change it (`403 forbidden` with code `customer_not_allowed`, checked before anything is saved); anyone else may send the stored value back, which leaves the flag as it is. true puts every domain of the account on the workspace blocklist with reason `customer`, so Ken sends its people nothing; false takes off only those rows, and a domain blocked for another reason stays blocked. The response carries `isCustomer`, `customerSince` and `customerSource` (manual here). `accountType` (prospect, client, lost, churned, partner, competitor, investor, do_not_contact; not null) is what the workspace calls the account, written after `isCustomer`. `client` is the customer flag: moving an account to client marks it a customer (its domains go on the blocklist) and moving a client to another type clears it, so either needs a workspace admin (`403 forbidden` with code `customer_not_allowed`, checked before anything is saved); the flag turning on or off any other way (a won opportunity, the CRM, `isCustomer`) moves the type to client or back to prospect. Any other type change is open to anyone with accounts edit. `accountType` and `isCustomer` that disagree in one body are `400 validation_failed`. Each change writes `account.type_changed` on the account's timeline. Only `client` changes sending by itself: any other type blocks the account's domains only while an enabled blocklist rule names it (`accountTypes` on `/v1/blocklist-rules`, DEV-4105), except that a client moved to lost, churned, competitor or do_not_contact keeps its domains on the blocklist. Sending the type the account already has changes nothing. To stop contacting an account, put its domains on the blocklist.
1384
+ Fields: name, stage, ownerMemberId (a workspace member), fitScore (0-100), whyNow, angle, cadenceDays (1-365), industry, fundingRound (1-100 characters), employeeCount, hqLocation, linkedinUrl, kenCompanyId (the company's Kenso id), sendsFromInboxId, sendsFromLinkedInAccountId. `sendsFromInboxId` is the inbox email touches send from: a member's primary inbox (owned and on the workspace's primary domain), or a shared campaign inbox on one of the workspace's cold sending domains, whose touches the account owner approves. `sendsFromLinkedInAccountId` is the LinkedIn account LinkedIn touches send from, connected by a member. Null on either means the owner's default. Neither is pinned, so an agent may change them. A bad id is `400 validation_failed` with code `not_found`, `not_owned`, `not_member` (its member left the workspace) or `email_domain_mismatch` (the inbox is not on the workspace's email domain; the message says what fixes it). Only a changed id is checked, so a form that sends the stored ids back saves. A disconnected sender is accepted; its touches wait until it reconnects. Changing a field a person set is `409 conflict` with `pinned_by_user` unless `overwritePinned: true`; nothing is applied partially. With account scope `own`, an account the caller does not own is `404 not_found`, and an `ownerMemberId` other than the caller's own is `403 permission_denied` with `scope: all`. Requires Accounts edit. `tierOverride` (1 to the ladder's `tierCount`; a higher tier is `400 validation_failed`) sets the account's tier by hand after the other fields are saved, and routes it to that tier's destination at once where the workspace has tier routing on (account-tiers); null is Auto: the account goes back to its auto tier (the tier the ladder placed it at, else its score's tier) without routing, and the ladder moves it from then on. Both write a tier change to the account's activity, with tier routing on or off. A tier whose move adds the account's people to a campaign, or stops the people the ladder put in one, also requires campaigns edit (`403 permission_denied`); both are checked before anything is saved. The response's `tier` is the hand-set tier, else `autoTier`. When another tier change on the account ran at the same time, the tier is not set and the response is `409 conflict`; the other fields are saved. A campaign that refuses the people (the plan's lead limit) also leaves the tier unset, with the other fields saved. `isCustomer` (true or false) marks the account a customer or clears it, after the other fields are saved. Only a workspace admin can change it (`403 forbidden` with code `customer_not_allowed`, checked before anything is saved); anyone else may send the stored value back, which leaves the flag as it is. true puts every domain of the account on the workspace blocklist with reason `customer`, so Ken sends its people nothing; false takes off only those rows, and a domain blocked for another reason stays blocked. The response carries `isCustomer`, `customerSince` and `customerSource` (manual here). `accountType` (prospect, client, lost, churned, partner, competitor, investor, do_not_contact; not null) is what the workspace calls the account, written after `isCustomer`. `client` is the customer flag: moving an account to client marks it a customer (its domains go on the blocklist) and moving a client to another type clears it, so either needs a workspace admin (`403 forbidden` with code `customer_not_allowed`, checked before anything is saved); the flag turning on or off any other way (a won opportunity, the CRM, `isCustomer`) moves the type to client or back to prospect. Any other type change is open to anyone with accounts edit. `accountType` and `isCustomer` that disagree in one body are `400 validation_failed`. Each change writes `account.type_changed` on the account's timeline. Only `client` changes sending by itself: any other type blocks the account's domains only while an enabled blocklist rule names it (`accountTypes` on `/v1/blocklist-rules`, DEV-4105), except that a client moved to lost, churned, competitor or do_not_contact keeps its domains on the blocklist. Sending the type the account already has changes nothing. To stop contacting an account, put its domains on the blocklist.
1384
1385
 
1385
1386
  - Beta: needs the `accounts` feature flag on the workspace.
1386
1387
  - API: `PATCH /v1/accounts/{accountId}`
@@ -1892,7 +1893,7 @@ kenso agent-team update-settings --data @body.json
1892
1893
 
1893
1894
  Reserve one AI assist call for the calling member.
1894
1895
 
1895
- Counts against the caller's per-user quota for the feature (`onboarding_interview`, `build_chat_suggestions`, `ai_search_filters`, or `builder_chat`): 10 to 20 a minute and 200 to 300 a rolling day. Nothing else is saved. An unknown feature is `400 validation_failed`. A spent quota is `429 quota_exceeded` with `Retry-After` and a top-level `quotaResetAt`, the same shape as `POST /v1/support/turns`. Super admins and workspaces with billing bypass are exempt. Requires `ReadAccess`.
1896
+ Counts against the caller's per-user quota for the feature (`onboarding_interview`, `build_chat_suggestions`, `ai_search_filters`, or `builder_chat`): 10 to 20 a minute and 200 to 300 a rolling day. Nothing else is saved. An unknown feature is `400 validation_failed`. A spent quota is `429 quota_exceeded` with `Retry-After` and a top-level `quotaResetAt`, the same shape as `POST /v1/support/turns`. Super admins and workspaces with billing bypass are exempt. Requires the `ask` permission.
1896
1897
 
1897
1898
  - API: `POST /v1/ai-assist/reservations`
1898
1899
  - Generated from the API contract
@@ -1916,7 +1917,7 @@ kenso ai-assist-reservations create --data @body.json
1916
1917
 
1917
1918
  Estimate prompt tokens and list the models a flow can use.
1918
1919
 
1919
- Internal. Replaces token analysis and supported models. `flow` is required. A campaign outside `X-Ken-Workspace-Id` is `not_found` before either read. Requires `ReadAccess`.
1920
+ Internal. Replaces token analysis and supported models. `flow` is required. A campaign outside `X-Ken-Workspace-Id` is `not_found` before either read. Requires Campaigns view.
1920
1921
 
1921
1922
  - API: `GET /v1/campaigns/{campaignId}/ai-cost-estimate`
1922
1923
  - Generated from the API contract
@@ -1939,7 +1940,7 @@ kenso ai-cost-estimate get --campaign-id <campaign-id>
1939
1940
 
1940
1941
  Approve a held contact's lines.
1941
1942
 
1942
- The lines the hold keeps become the contact's personalization lines and the contact counts as personalized, so it can send. Not charged. A contact with no hold in the campaign (never held, already approved, or regenerated) is `not_held` (409). When the contact already has a live line for one of the held variables the approval is `conflict` (409) and nothing changes; regenerate the contact instead. A campaign or contact outside the workspace, or a contact not in this campaign, is `not_found`. Requires `WriteAccess`.
1943
+ The lines the hold keeps become the contact's personalization lines and the contact counts as personalized, so it can send. Not charged. A contact with no hold in the campaign (never held, already approved, or regenerated) is `not_held` (409). When the contact already has a live line for one of the held variables the approval is `conflict` (409) and nothing changes; regenerate the contact instead. A campaign or contact outside the workspace, or a contact not in this campaign, is `not_found`. Requires Campaigns edit.
1943
1944
 
1944
1945
  - API: `POST /v1/campaigns/{campaignId}/ai-line-holds/{contactId}/approve`
1945
1946
  - Generated from the API contract
@@ -1959,7 +1960,7 @@ kenso ai-line-holds approve --campaign-id <campaign-id> --contact-id <contact-id
1959
1960
 
1960
1961
  List the contacts the AI line check held in a campaign.
1961
1962
 
1962
- Newest hold first, with the reasons and the lines each hold keeps. Pages with `limit` (1 to 200, default 50) and `cursor`. A contact appears until it is approved or regenerated. A campaign outside the workspace is `not_found`. Requires `ReadAccess`.
1963
+ Newest hold first, with the reasons and the lines each hold keeps. Pages with `limit` (1 to 200, default 50) and `cursor`. A contact appears until it is approved or regenerated. A campaign outside the workspace is `not_found`. Requires Campaigns view.
1963
1964
 
1964
1965
  - API: `GET /v1/campaigns/{campaignId}/ai-line-holds` (a list: `--limit`, `--cursor`, `--all`)
1965
1966
  - Generated from the API contract
@@ -1984,7 +1985,7 @@ kenso ai-line-holds list --campaign-id <campaign-id> --all
1984
1985
 
1985
1986
  List a contact's AI lines.
1986
1987
 
1987
- One line per AI variable of the contact's campaign: the stored line its next send renders (the newest personalized line, or its rewrite when the prompt rewrites; the segment's prompt before the campaign's). Older runs and replaced lines are not listed. Ordered by variable, at most 200 lines, not paginated. A contact outside the workspace, deleted, or not in a campaign is `not_found`. Requires `ReadAccess`.
1988
+ One line per AI variable of the contact's campaign: the stored line its next send renders (the newest personalized line, or its rewrite when the prompt rewrites; the segment's prompt before the campaign's). Older runs and replaced lines are not listed. Ordered by variable, at most 200 lines, not paginated. A contact outside the workspace, deleted, or not in a campaign is `not_found`. Requires Campaigns view.
1988
1989
 
1989
1990
  - Beta: needs the `ai_line_edit` feature flag on the workspace.
1990
1991
  - API: `GET /v1/contacts/{contactId}/ai-lines`
@@ -2004,7 +2005,7 @@ kenso ai-lines list --contact-id <contact-id>
2004
2005
 
2005
2006
  Write a contact's AI lines again.
2006
2007
 
2007
- Clears the contact's personalized lines (and the rewritten lines built on them) and queues a personalization run for that contact alone, charged as one lead. Returns `202` with the run; poll `GET /v1/ai-runs/{aiRunId}`. When the run lands, an approval draft that shows the lead is updated. A lead already sent or exported, with a send queued, or in an unfinished AI run is `409 invalid_state` and nothing changes. AI quota limits answer `429 quota_exceeded` and too few credits `402`, both before anything is cleared, so the lead keeps its lines. Only a full AI run queue (`503`) after the clear leaves the lead without lines until a retry or the next scheduled AI run. Requires `WriteAccess`.
2008
+ Clears the contact's personalized lines (and the rewritten lines built on them) and queues a personalization run for that contact alone, charged as one lead. Returns `202` with the run; poll `GET /v1/ai-runs/{aiRunId}`. When the run lands, an approval draft that shows the lead is updated. A lead already sent or exported, with a send queued, or in an unfinished AI run is `409 invalid_state` and nothing changes. AI quota limits answer `429 quota_exceeded` and too few credits `402`, both before anything is cleared, so the lead keeps its lines. Only a full AI run queue (`503`) after the clear leaves the lead without lines until a retry or the next scheduled AI run. Requires Campaigns edit and the `ask` permission.
2008
2009
 
2009
2010
  - Beta: needs the `ai_line_edit` feature flag on the workspace.
2010
2011
  - API: `POST /v1/contacts/{contactId}/ai-lines/regenerate`
@@ -2024,7 +2025,7 @@ kenso ai-lines regenerate --contact-id <contact-id>
2024
2025
 
2025
2026
  Replace the text of one of a contact's AI lines.
2026
2027
 
2027
- The next send of the lead renders the new text, and an approval draft that shows the lead is updated. Steps already sent keep what they sent. `text` is required, at most 4,000 characters; it gets the cleanup a generated line gets (emoji removed, dashes normalized, spaces collapsed) and must be 1 to 1,000 characters after it, else `validation_failed`. Another of the contact's lines that the send does not read (an older run, a personalized line its rewrite replaces) is `409 invalid_state` and nothing changes; list the lines again and edit the one shown. A line that is not the contact's is `not_found`. Requires `WriteAccess`.
2028
+ The next send of the lead renders the new text, and an approval draft that shows the lead is updated. Steps already sent keep what they sent. `text` is required, at most 4,000 characters; it gets the cleanup a generated line gets (emoji removed, dashes normalized, spaces collapsed) and must be 1 to 1,000 characters after it, else `validation_failed`. Another of the contact's lines that the send does not read (an older run, a personalized line its rewrite replaces) is `409 invalid_state` and nothing changes; list the lines again and edit the one shown. A line that is not the contact's is `not_found`. Requires Campaigns edit.
2028
2029
 
2029
2030
  - Beta: needs the `ai_line_edit` feature flag on the workspace.
2030
2031
  - API: `PATCH /v1/contacts/{contactId}/ai-lines/{aiLineId}`
@@ -2051,7 +2052,7 @@ kenso ai-lines update --contact-id <contact-id> --ai-line-id <ai-line-id> --data
2051
2052
 
2052
2053
  Queue an AI run for a campaign in the current workspace.
2053
2054
 
2054
- Returns `202` with a run id. Poll `GET /v1/ai-runs/{aiRunId}`. A campaign outside `X-Ken-Workspace-Id` is `not_found` before the run is queued. With `dryRun: true` the request is validated the same way and the answer is `200` with what the run would process: `batchSize` (the requested batch, else the flow's submission limit when `processAll`, else 500), `eligibleCount` (eligible contacts now, in `segmentId` when set), `projectedContactCount` (the lower of the two, an upper bound) and the flow's `blockingGate` and `blockingReason`. Nothing is queued, no quota is reserved and no credits are spent. Requires `WriteAccess`.
2055
+ Returns `202` with a run id. Poll `GET /v1/ai-runs/{aiRunId}`. A campaign outside `X-Ken-Workspace-Id` is `not_found` before the run is queued. With `dryRun: true` the request is validated the same way and the answer is `200` with what the run would process: `batchSize` (the requested batch, else the flow's submission limit when `processAll`, else 500), `eligibleCount` (eligible contacts now, in `segmentId` when set), `projectedContactCount` (the lower of the two, an upper bound) and the flow's `blockingGate` and `blockingReason`. Nothing is queued, no quota is reserved and no credits are spent. Requires Campaigns edit and the `ask` permission.
2055
2056
 
2056
2057
  - API: `POST /v1/campaigns/{campaignId}/ai-runs`
2057
2058
  - Generated from the API contract
@@ -2063,7 +2064,7 @@ Returns `202` with a run id. Poll `GET /v1/ai-runs/{aiRunId}`. A campaign outsid
2063
2064
  | `--flow` | string | no | flow (body, string) |
2064
2065
  | `--batch-size` | integer | no | batchSize (body, integer) |
2065
2066
  | `--process-all` | boolean | no | processAll (body, boolean) |
2066
- | `--segment-id` | integer | no | segmentId (body, integer) |
2067
+ | `--segment-id` | integer | no | A segment of the campaign. Omit it for every segment; `0` is `400 validation_failed`, not "no segment". |
2067
2068
  | `--provider` | string | no | provider (body, string) |
2068
2069
  | `--model` | string | no | model (body, string) |
2069
2070
  | `--body-dry-run` | boolean | no | True validates the run and returns what it would process (`200`), without queueing it, reserving quota or spending credits. Default false. |
@@ -2082,7 +2083,7 @@ kenso ai-runs create --campaign-id <campaign-id> --data @body.json
2082
2083
 
2083
2084
  Read one AI run.
2084
2085
 
2085
- The run's campaign must belong to `X-Ken-Workspace-Id`. A missing run, or a run outside the workspace, is `not_found`. Requires `ReadAccess`.
2086
+ The run's campaign must belong to `X-Ken-Workspace-Id`. A missing run, or a run outside the workspace, is `not_found`. Requires Campaigns view.
2086
2087
 
2087
2088
  - API: `GET /v1/ai-runs/{aiRunId}`
2088
2089
  - Generated from the API contract
@@ -2101,7 +2102,7 @@ kenso ai-runs get --ai-run-id <ai-run-id>
2101
2102
 
2102
2103
  Read AI-run eligibility for a campaign, with an optional invalidation preview.
2103
2104
 
2104
- Replaces eligible counts and the invalidation preview. Pass `flow` and `preview` (`freshness` or `reset`) to include the preview. A campaign outside the workspace is `not_found` before either read. Requires `ReadAccess`.
2105
+ Replaces eligible counts and the invalidation preview. Pass `flow` and `preview` (`freshness` or `reset`) to include the preview. A campaign outside the workspace is `not_found` before either read. Requires Campaigns view.
2105
2106
 
2106
2107
  - API: `GET /v1/campaigns/{campaignId}/ai-runs/eligibility`
2107
2108
  - Generated from the API contract
@@ -2111,7 +2112,7 @@ Replaces eligible counts and the invalidation preview. Pass `flow` and `preview`
2111
2112
  | `--campaign-id` | integer | yes | campaignId (path, integer) |
2112
2113
  | `--flow` | string | no | flow (query, string) |
2113
2114
  | `--preview` | string | no | preview (query, string) |
2114
- | `--segment-id` | integer | no | segmentId (query, integer) |
2115
+ | `--segment-id` | integer | no | A segment of the campaign. Omit it for every segment; `0` is `400 validation_failed`, not "no segment". |
2115
2116
  | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
2116
2117
  | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
2117
2118
 
@@ -2289,7 +2290,7 @@ kenso analytics get-deliverability
2289
2290
 
2290
2291
  Return the bounce gates in force and each domain's and inbox's verdict for the last 24 hours.
2291
2292
 
2292
- Read-only (DEV-5210). Counts accepted cold and 1:1 sends and hard bounces over the last 24 hours, as the sequencer's bounce signal does, and judges each row by the gate the sequencer enforces (`Sequencer:SendBlocks:Bounce`): inbox rows by the inbox block gate, domain rows by the campaign pause gate (there is no domain bounce block). A row is `over` when its rate is strictly above `threshold` with at least `minSends` sends and `minBounces` hard bounces, `watch` when it is not over and its rate is at or above 80% of the threshold, and `ok` otherwise. `isSmallSample` marks a row with fewer sends than `minSends`, which cannot be `over`. The sequencer itself judges over `enforcedWindowHours`; `isEnforced` is false while it only records. A mailbox with no managed domain counts toward its address's domain, except on a free-mail provider (gmail.com), where it has an inbox row only. Rates are fractions from 0 to 1. Rows are worst first; `limit` caps each list (1 to 200, default 50).
2293
+ Read-only (DEV-5210). Counts accepted cold and 1:1 sends and hard bounces over the last 24 hours, as the sequencer's bounce signal does, and judges each row by the bounce gates the sequencer enforces: inbox rows by the inbox block gate, domain rows by the campaign pause gate (there is no domain bounce block). A row is `over` when its rate is strictly above `threshold` with at least `minSends` sends and `minBounces` hard bounces, `watch` when it is not over and its rate is at or above 80% of the threshold, and `ok` otherwise. `isSmallSample` marks a row with fewer sends than `minSends`, which cannot be `over`. The sequencer itself judges over `enforcedWindowHours`; `isEnforced` is false while it only records. A mailbox with no managed domain counts toward its address's domain, except on a free-mail provider (gmail.com), where it has an inbox row only. Rates are fractions from 0 to 1. Rows are worst first; `limit` caps each list (1 to 200, default 50).
2293
2294
 
2294
2295
  - API: `GET /v1/analytics/deliverability-gates`
2295
2296
  - Generated from the API contract
@@ -2644,7 +2645,7 @@ kenso apollo-lists list
2644
2645
 
2645
2646
  Advance a draft through one AI flow.
2646
2647
 
2647
- Requires `WriteAccess`.
2648
+ Requires Campaigns edit.
2648
2649
 
2649
2650
  - API: `POST /v1/approval-tests/{approvalTestId}/advance`
2650
2651
  - Generated from the API contract
@@ -2669,7 +2670,7 @@ kenso approval-tests advance --approval-test-id <approval-test-id> --data @body.
2669
2670
 
2670
2671
  Create an approval draft for a campaign in the current workspace.
2671
2672
 
2672
- Returns `201` and `Location`. Requires `WriteAccess`.
2673
+ Returns `201` and `Location`. Requires Campaigns edit and the `share` permission.
2673
2674
 
2674
2675
  - API: `POST /v1/campaigns/{campaignId}/approval-tests`
2675
2676
  - Generated from the API contract
@@ -2699,7 +2700,7 @@ kenso approval-tests create --campaign-id <campaign-id> --data @body.json
2699
2700
 
2700
2701
  Read a draft's client feedback.
2701
2702
 
2702
- Feedback rows have no numeric id, so this returns the full window with no cursor and no total count. Requires `WriteAccess`.
2703
+ Feedback rows have no numeric id, so this returns the full window with no cursor and no total count. Requires Campaigns view.
2703
2704
 
2704
2705
  - API: `GET /v1/approval-tests/{approvalTestId}/feedback`
2705
2706
  - Generated from the API contract
@@ -2760,7 +2761,7 @@ kenso approval-tests list --campaign-id <campaign-id> --all
2760
2761
 
2761
2762
  Rotate a draft's share-link token.
2762
2763
 
2763
- Requires `WriteAccess`.
2764
+ Requires Campaigns edit and the `share` permission.
2764
2765
 
2765
2766
  - API: `POST /v1/approval-tests/{approvalTestId}/rotate-link`
2766
2767
  - Generated from the API contract
@@ -4070,7 +4071,7 @@ kenso ats-pushes create --data @body.json
4070
4071
 
4071
4072
  Upload a pending attachment for an email message.
4072
4073
 
4073
- Multipart field `file` and form field `messageId` (`msg_`, email only). Caps stay at 5 files, 8 MiB each, and 10 MiB pending. Requires `InboxReplyAccess`.
4074
+ Multipart field `file` and form field `messageId` (`msg_`, email only). Caps stay at 5 files, 8 MiB each, and 10 MiB pending. Requires Inbox edit.
4074
4075
 
4075
4076
  - API: `POST /v1/attachments`
4076
4077
  - Generated from the API contract
@@ -4092,7 +4093,7 @@ kenso attachments create --file ./file.csv
4092
4093
 
4093
4094
  Delete a pending attachment that has not been sent.
4094
4095
 
4095
- Returns `204`. A malformed id is `not_found`. Requires `InboxReplyAccess`.
4096
+ Returns `204`. A malformed id is `not_found`. Requires Inbox edit.
4096
4097
 
4097
4098
  - API: `DELETE /v1/attachments/{attachmentId}`
4098
4099
  - Generated from the API contract
@@ -4111,7 +4112,7 @@ kenso attachments delete --attachment-id <attachment-id>
4111
4112
 
4112
4113
  Download attachment bytes.
4113
4114
 
4114
- A malformed id is `not_found`. `inline=true` is only honored for an inline image. Requires `ReadAccess`.
4115
+ A malformed id is `not_found`. `inline=true` is only honored for an inline image. Requires Inbox view.
4115
4116
 
4116
4117
  - API: `GET /v1/attachments/{attachmentId}/content`
4117
4118
  - Generated from the API contract
@@ -4261,7 +4262,7 @@ kenso auto-pauses list --all
4261
4262
 
4262
4263
  Return autonomous reply settings for the current workspace.
4263
4264
 
4264
- Requires `ReadAccess`.
4265
+ Open to any workspace member.
4265
4266
 
4266
4267
  - API: `GET /v1/autonomous-reply-settings`
4267
4268
  - Generated from the API contract
@@ -4279,7 +4280,7 @@ kenso autonomous-reply-settings get
4279
4280
 
4280
4281
  Update autonomous reply settings.
4281
4282
 
4282
- Absent fields stay unchanged. JSON null clears `extraInstructions`. `mode` is `draft` or `autonomous`, or `approve_risky` where the workspace has the `reply_agent_approve_risky` flag on; elsewhere `approve_risky` gets the same `400 validation_failed` on `mode` as an unknown mode. A body without `mode` keeps the stored mode. Requires `WriteAccess`.
4283
+ Absent fields stay unchanged. JSON null clears `extraInstructions`. `mode` is `draft` or `autonomous`, or `approve_risky` where the workspace has the `reply_agent_approve_risky` flag on; elsewhere `approve_risky` gets the same `400 validation_failed` on `mode` as an unknown mode. A body without `mode` keeps the stored mode. Requires the `workspace_settings` permission.
4283
4284
 
4284
4285
  - API: `PATCH /v1/autonomous-reply-settings`
4285
4286
  - Generated from the API contract
@@ -4404,7 +4405,7 @@ kenso blitz-people search --data @body.json
4404
4405
 
4405
4406
  Add up to 1,000 blocklist values of one type.
4406
4407
 
4407
- Send `values`, or `entries` (`[{ "value": "...", "reason": "..." }]`) for a reason per value, not both. Optional `reason` applies to every value without its own. Per-value outcomes are `succeeded` or `failed`. Duplicates stay failed with `already_exists` and keep their own reason. A domain a customer account or a blocklist rule already lists becomes `manual` under its id, with the reason sent, and is in `succeeded` with `claimed: true`, as on the single add. A shared mail provider domain (gmail.com, hotmail.co.uk, web.de) fails with `shared_mail_provider`; any other malformed value with `invalid`. To undo the add, bulk-delete only the `succeeded` ids without `claimed: true`: a claimed entry was blocked before the add, and deleting it unblocks the domain. Requires `WriteAccess`.
4408
+ Send `values`, or `entries` (`[{ "value": "...", "reason": "..." }]`) for a reason per value, not both. Optional `reason` applies to every value without its own. Per-value outcomes are `succeeded` or `failed`. Duplicates stay failed with `already_exists` and keep their own reason. A domain a customer account or a blocklist rule already lists becomes `manual` under its id, with the reason sent, and is in `succeeded` with `claimed: true`, as on the single add. A shared mail provider domain (gmail.com, hotmail.co.uk, web.de) fails with `shared_mail_provider`; any other malformed value with `invalid`. To undo the add, bulk-delete only the `succeeded` ids without `claimed: true`: a claimed entry was blocked before the add, and deleting it unblocks the domain. Requires the `workspace_settings` permission.
4408
4409
 
4409
4410
  - API: `POST /v1/blocklist-entries/bulk-create`
4410
4411
  - Generated from the API contract
@@ -4427,7 +4428,7 @@ kenso blocklist-entries bulk-create --data @body.json
4427
4428
 
4428
4429
  Remove up to 1,000 blocklist entries by id.
4429
4430
 
4430
- Body `{ "ids": [...] }`. Removes them in one transaction and runs the own-domain and rule reconciles once, so undoing a large upload takes one call per 1,000 entries. To undo a bulk add, send only its `succeeded` ids without `claimed: true`: a claimed entry was blocked before the add (by a customer account or a rule), and a customer entry deleted here stays off. `succeeded` lists every id that is off the blocklist after the call, in request order, including an id already gone. An own-domain entry, and a rule entry while blocklist rules are on, stay and are in `failed` with `invalid_state` and the single delete's message. Requires `WriteAccess`.
4431
+ Body `{ "ids": [...] }`. Removes them in one transaction and runs the own-domain and rule reconciles once, so undoing a large upload takes one call per 1,000 entries. To undo a bulk add, send only its `succeeded` ids without `claimed: true`: a claimed entry was blocked before the add (by a customer account or a rule), and a customer entry deleted here stays off. `succeeded` lists every id that is off the blocklist after the call, in request order, including an id already gone. An own-domain entry, and a rule entry while blocklist rules are on, stay and are in `failed` with `invalid_state` and the single delete's message. Requires the `workspace_settings` permission.
4431
4432
 
4432
4433
  - API: `POST /v1/blocklist-entries/bulk-delete`
4433
4434
  - Generated from the API contract
@@ -4448,7 +4449,7 @@ kenso blocklist-entries bulk-delete --data @body.json
4448
4449
 
4449
4450
  Add one email or domain to the blocklist.
4450
4451
 
4451
- Optional `reason` (at most 200 characters) says why the value is blocked. A duplicate entry is `validation_failed` and keeps its own reason. A domain a customer account or a blocklist rule already lists becomes `manual` under its id, with this reason, and comes back with `claimed: true`. A shared mail provider domain (gmail.com, hotmail.co.uk, web.de) is `validation_failed` with detail code `shared_mail_provider`: block those addresses one by one. Requires `WriteAccess`.
4452
+ Optional `reason` (at most 200 characters) says why the value is blocked. A duplicate entry is `validation_failed` and keeps its own reason. A domain a customer account or a blocklist rule already lists becomes `manual` under its id, with this reason, and comes back with `claimed: true`. A shared mail provider domain (gmail.com, hotmail.co.uk, web.de) is `validation_failed` with detail code `shared_mail_provider`: block those addresses one by one. Requires the `workspace_settings` permission.
4452
4453
 
4453
4454
  - API: `POST /v1/blocklist-entries`
4454
4455
  - Generated from the API contract
@@ -4472,7 +4473,7 @@ kenso blocklist-entries create --data @body.json
4472
4473
 
4473
4474
  Remove one blocklist entry.
4474
4475
 
4475
- Requires `WriteAccess`. An entry outside the workspace is `not_found`.
4476
+ Requires the `workspace_settings` permission. An entry outside the workspace is `not_found`.
4476
4477
 
4477
4478
  - API: `DELETE /v1/blocklist-entries/{entryId}`
4478
4479
  - Generated from the API contract
@@ -4491,7 +4492,7 @@ kenso blocklist-entries delete --entry-id <entry-id>
4491
4492
 
4492
4493
  Import blocklist rows from a CSV file.
4493
4494
 
4494
- Multipart field `file` is a `.csv` with `email` and/or `domain` columns and an optional `reason` column; a cell over 200 characters is cut, never failing the file. Optional form field `reason` (at most 200 characters) applies to every row without its own. Rows already on the blocklist keep their own reason, and a shared mail provider in the `domain` column (gmail.com) is skipped. Requires `WriteAccess`.
4495
+ Multipart field `file` is a `.csv` with `email` and/or `domain` columns and an optional `reason` column; a cell over 200 characters is cut, never failing the file. Optional form field `reason` (at most 200 characters) applies to every row without its own. Rows already on the blocklist keep their own reason, and a shared mail provider in the `domain` column (gmail.com) is skipped. Requires the `workspace_settings` permission.
4495
4496
 
4496
4497
  - API: `POST /v1/blocklist-entries/imports`
4497
4498
  - Generated from the API contract
@@ -4513,7 +4514,7 @@ kenso blocklist-entries import --file ./file.csv
4513
4514
 
4514
4515
  List blocklist entries in the current workspace.
4515
4516
 
4516
- Ordered by id, which follows creation: `sort=createdAt` (the default) is oldest first, `sort=-createdAt` newest first; keep the same `sort` while paging. Optional filters: `type` (`email` or `domain`), `value` (an exact match), `q` (a case-insensitive substring of the value or the reason), `source` (comma-separated `manual`, `own_domain`, `customer`, `rule`) and `hasReason`. `includeTotal=true` adds `pagination.total`, every entry the filters match. `includeCounts=true` adds `counts`: the workspace's entries in all, emails, domains, from the CRM (`customer` and `rule`) and with no reason, whatever the filters. Requires `ReadAccess`.
4517
+ Ordered by id, which follows creation: `sort=createdAt` (the default) is oldest first, `sort=-createdAt` newest first; keep the same `sort` while paging. Optional filters: `type` (`email` or `domain`), `value` (an exact match), `q` (a case-insensitive substring of the value or the reason), `source` (comma-separated `manual`, `own_domain`, `customer`, `rule`) and `hasReason`. `includeTotal=true` adds `pagination.total`, every entry the filters match. `includeCounts=true` adds `counts`: the workspace's entries in all, emails, domains, from the CRM (`customer` and `rule`) and with no reason, whatever the filters. Open to any workspace member.
4517
4518
 
4518
4519
  - API: `GET /v1/blocklist-entries` (a list: `--limit`, `--cursor`, `--all`)
4519
4520
  - Generated from the API contract
@@ -4546,7 +4547,7 @@ kenso blocklist-entries list --all
4546
4547
 
4547
4548
  Set or clear why a blocklist entry is blocked.
4548
4549
 
4549
- Body `{ "reason": "..." }`, at most 200 characters; `null` or blank clears it. `reason` is required. Requires `WriteAccess`. An entry outside the workspace is `not_found`.
4550
+ Body `{ "reason": "..." }`, at most 200 characters; `null` or blank clears it. `reason` is required. Requires the `workspace_settings` permission. An entry outside the workspace is `not_found`.
4550
4551
 
4551
4552
  - API: `PATCH /v1/blocklist-entries/{entryId}`
4552
4553
  - Generated from the API contract
@@ -4619,7 +4620,7 @@ kenso blocklist-rules delete --rule-id <rule-id>
4619
4620
 
4620
4621
  List blocklist rules.
4621
4622
 
4622
- Every rule in `position` order (at most 20), each with `stages`, `accountTypes`, `crmLifecycles`, `crmList` (with the `members` and `readAt` of its last read) and `matches`: the accounts its stages (and release window, `releaseAfterDays`) or account types match now (each once), the domains and people (emails) it blocks and the CRM records it matches, enabled or not. The first enabled rule that matches a domain or email gives its entry the reason. Requires `ReadAccess`.
4623
+ Every rule in `position` order (at most 20), each with `stages`, `accountTypes`, `crmLifecycles`, `crmList` (with the `members` and `readAt` of its last read) and `matches`: the accounts its stages (and release window, `releaseAfterDays`) or account types match now (each once), the domains and people (emails) it blocks and the CRM records it matches, enabled or not. The first enabled rule that matches a domain or email gives its entry the reason. Open to any workspace member.
4623
4624
 
4624
4625
  - Beta: needs the `blocklist_rules` feature flag on the workspace.
4625
4626
  - API: `GET /v1/blocklist-rules`
@@ -4638,7 +4639,7 @@ kenso blocklist-rules list
4638
4639
 
4639
4640
  Preview what a blocklist rule would block.
4640
4641
 
4641
- `stages` is a comma-separated list of up to 20 stage keys from `GET /v1/opportunity-stages`; an unknown or archived key is `400 out_of_range`. `accountTypes` is a comma-separated list of account types (`client`, `lost`, `churned`, `partner`, `competitor`, `investor`, `do_not_contact`; `prospect` or an unknown type is `400 out_of_range`). Send at least one of the two, or a CRM criterion: `crmLifecycles` is a comma-separated list of up to 5 CRM lifecycle stages (`lead`, `prospect`, `opportunity`, `customer`, `churned`) and `crmListId` a contact or company list of the connected CRM (`GET /v1/crm-connections/{connectionId}/lists`, at most 10,000 members, read live now and never stored). A CRM criterion while `blocklist_rules_crm` is off is `400 not_enabled`; with no connected CRM `400 invalid_state`; on a CRM Kenso cannot read lists or lifecycle stages from `400 unsupported`; an unknown or too large list `400 out_of_range`; a CRM that is not reachable or rate limited `503` or `429`. Returns the accounts with a live opportunity in one of the stages or of one of the types (each once), the distinct domains and people (emails) a rule would block, the CRM records it matches (`crmRecords`), how many of the domains and people a blocklist entry already lists (`alreadyBlocked`, any source), up to 10 accounts by id with one of their domains, up to 10 CRM records with the email or domain each blocks (`crmSample`), and what is left out: accounts and CRM companies with no domain, CRM contacts with neither a company domain nor an email (`skipped.noEmail`) and free email domains. Optional `releaseAfterDays` (1-3650) previews a release window: an account matched by stage counts only while one of its deals in the stages closed less than that many days ago or has no close date (a match by type or from the CRM never releases), and `skipped.released` counts the accounts it already let go. Never writes. Requires `ReadAccess` (accounts view), since it names accounts.
4642
+ `stages` is a comma-separated list of up to 20 stage keys from `GET /v1/opportunity-stages`; an unknown or archived key is `400 out_of_range`. `accountTypes` is a comma-separated list of account types (`client`, `lost`, `churned`, `partner`, `competitor`, `investor`, `do_not_contact`; `prospect` or an unknown type is `400 out_of_range`). Send at least one of the two, or a CRM criterion: `crmLifecycles` is a comma-separated list of up to 5 CRM lifecycle stages (`lead`, `prospect`, `opportunity`, `customer`, `churned`) and `crmListId` a contact or company list of the connected CRM (`GET /v1/crm-connections/{connectionId}/lists`, at most 10,000 members, read live now and never stored). A CRM criterion while `blocklist_rules_crm` is off is `400 not_enabled`; with no connected CRM `400 invalid_state`; on a CRM Kenso cannot read lists or lifecycle stages from `400 unsupported`; an unknown or too large list `400 out_of_range`; a CRM that is not reachable or rate limited `503` or `429`. Returns the accounts with a live opportunity in one of the stages or of one of the types (each once), the distinct domains and people (emails) a rule would block, the CRM records it matches (`crmRecords`), how many of the domains and people a blocklist entry already lists (`alreadyBlocked`, any source), up to 10 accounts by id with one of their domains, up to 10 CRM records with the email or domain each blocks (`crmSample`), and what is left out: accounts and CRM companies with no domain, CRM contacts with neither a company domain nor an email (`skipped.noEmail`) and free email domains. Optional `releaseAfterDays` (1-3650) previews a release window: an account matched by stage counts only while one of its deals in the stages closed less than that many days ago or has no close date (a match by type or from the CRM never releases), and `skipped.released` counts the accounts it already let go. Never writes. Requires Accounts view, since it names accounts.
4642
4643
 
4643
4644
  - Beta: needs the `blocklist_rules` feature flag on the workspace.
4644
4645
  - API: `GET /v1/blocklist-rules/preview`
@@ -5153,6 +5154,101 @@ Return the brand of a verified custom host.
5153
5154
  kenso brands get-by-host
5154
5155
  ```
5155
5156
 
5157
+ ## browser-runs
5158
+
5159
+ ### kenso browser-runs get
5160
+
5161
+ Return one browser run in full.
5162
+
5163
+ The run with its task, identity and inbox, the structured `result`, `quotes` with their URLs, `screenshots` (`url` is the screenshot route), the replay link while it is kept (`replayExpiresAt`), the `submission` (the form URL and the fields sent), the watched domains, the first human reply and its seconds after the submit, and the error of a failed run. A run of another workspace is 404. Requires workflows view.
5164
+
5165
+ - Beta: needs the `browser_steps` and `workflows` feature flag on the workspace.
5166
+ - API: `GET /v1/browser-runs/{browserRunId}`
5167
+ - Generated from the API contract
5168
+
5169
+ | Flag | Type | Required | Description |
5170
+ | --- | --- | --- | --- |
5171
+ | `--browser-run-id` | integer | yes | browserRunId (path, integer) |
5172
+ | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
5173
+ | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
5174
+
5175
+ ```bash
5176
+ kenso browser-runs get --browser-run-id <browser-run-id>
5177
+ ```
5178
+
5179
+ ### kenso browser-runs list
5180
+
5181
+ List the workspace's browser runs, newest first.
5182
+
5183
+ Filters: `workflowId`, `runId` (the workflow run), `nodeId` (the browser step), `recordType` and `recordId`, `status` (one of queued, awaiting_approval, declined, running, done, confirmed, unconfirmed, blocked, out_of_browser_time, failed). Items carry no quotes, screenshots or submission: read one run for those. `limit` 1 to 200 (default 50); continue with `pagination.nextCursor`. Requires workflows view.
5184
+
5185
+ - Beta: needs the `browser_steps` and `workflows` feature flag on the workspace.
5186
+ - API: `GET /v1/browser-runs` (a list: `--limit`, `--cursor`, `--all`)
5187
+ - Generated from the API contract
5188
+
5189
+ | Flag | Type | Required | Description |
5190
+ | --- | --- | --- | --- |
5191
+ | `--cursor` | string | no | Start at this cursor (the next: hint of a previous page). |
5192
+ | `--limit` | integer | no | 1 to 200, default 50. |
5193
+ | `--node-id` | string | no | The browser step's node id in the workflow. |
5194
+ | `--record-id` | integer | no | recordId (query, integer) |
5195
+ | `--record-type` | string | no | account, person, lead, place or none. |
5196
+ | `--run-id` | integer | no | The workflow run the browser runs belong to. |
5197
+ | `--status` | string | no | One status: queued, awaiting_approval, declined, running, done, confirmed, unconfirmed, blocked, out_of_browser_time, failed. |
5198
+ | `--workflow-id` | integer | no | workflowId (query, integer) |
5199
+ | `--all` | boolean | no | Follow every page and stream all items (NDJSON when piped). |
5200
+ | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
5201
+ | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
5202
+
5203
+ ```bash
5204
+ kenso browser-runs list
5205
+ kenso browser-runs list --all
5206
+ ```
5207
+
5208
+ ### kenso browser-runs screenshot
5209
+
5210
+ One screenshot of a browser run, as JPEG.
5211
+
5212
+ `n` is 1-based, up to the run's `screenshotCount`. 404 when the run, the index or the file is not there. Cached privately for an hour. Requires workflows view.
5213
+
5214
+ - Beta: needs the `browser_steps` and `workflows` feature flag on the workspace.
5215
+ - API: `GET /v1/browser-runs/{browserRunId}/screenshots/{n}`
5216
+ - Generated from the API contract
5217
+
5218
+ | Flag | Type | Required | Description |
5219
+ | --- | --- | --- | --- |
5220
+ | `--browser-run-id` | integer | yes | browserRunId (path, integer) |
5221
+ | `--n` | integer | yes | n (path, integer) |
5222
+ | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
5223
+ | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
5224
+
5225
+ ```bash
5226
+ kenso browser-runs screenshot --browser-run-id <browser-run-id> --n <n>
5227
+ ```
5228
+
5229
+ ### kenso browser-runs summary
5230
+
5231
+ Runs by status and reply buckets for one workflow's browser steps.
5232
+
5233
+ Over the browser runs of `workflowId` (one step with `nodeId`) created in [`createdAfter`, `createdBefore`) (default the last 30 days, at most 400 days): `runs`, `byStatus`, `replies` (`withinHour`, `withinDay`, `afterDay` from the first human reply after the submit; `noneBeforeTimeout` submitted runs whose watch closed with no human reply; `watching` still open), credits and browser minutes. Requires workflows view.
5234
+
5235
+ - Beta: needs the `browser_steps` and `workflows` feature flag on the workspace.
5236
+ - API: `GET /v1/browser-runs/summary`
5237
+ - Generated from the API contract
5238
+
5239
+ | Flag | Type | Required | Description |
5240
+ | --- | --- | --- | --- |
5241
+ | `--created-after` | string | no | Inclusive; default 30 days before Ken.Scraper.API.Dtos.BrowserRunSummaryRequest.CreatedBefore. |
5242
+ | `--created-before` | string | no | Exclusive; default now. |
5243
+ | `--node-id` | string | no | One browser step's node id; every browser step of the workflow when absent. |
5244
+ | `--workflow-id` | integer | yes | Required: a request without it fails binding with the canonical 400 on `workflowId`; 0 is refused too. |
5245
+ | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
5246
+ | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
5247
+
5248
+ ```bash
5249
+ kenso browser-runs summary --workflow-id <workflow-id>
5250
+ ```
5251
+
5156
5252
  ## bug-reports
5157
5253
 
5158
5254
  ### kenso bug-reports create
@@ -5214,7 +5310,7 @@ kenso build-events append --build-session-id <build-session-id> --data @body.jso
5214
5310
 
5215
5311
  List durable UI events for a build session.
5216
5312
 
5217
- Ordered by `seq`. The cursor is the last returned seq. Read-only. Requires `ReadAccess`.
5313
+ Ordered by `seq`. The cursor is the last returned seq. Read-only. Requires Campaigns view.
5218
5314
 
5219
5315
  - API: `GET /v1/build-sessions/{buildSessionId}/events` (a list: `--limit`, `--cursor`, `--all`)
5220
5316
  - Generated from the API contract
@@ -5370,7 +5466,7 @@ kenso build-files search --all
5370
5466
 
5371
5467
  Return the workspace's active build session.
5372
5468
 
5373
- Optional `campaignId` narrows the lookup. Read-only. Requires `ReadAccess`.
5469
+ Optional `campaignId` narrows the lookup. Read-only. Requires Campaigns view.
5374
5470
 
5375
5471
  - API: `GET /v1/build-sessions/active`
5376
5472
  - Generated from the API contract
@@ -5389,7 +5485,7 @@ kenso build-sessions active
5389
5485
 
5390
5486
  Return the latest build session for one campaign.
5391
5487
 
5392
- One session, including a completed one. Read-only. Requires `ReadAccess`.
5488
+ One session, including a completed one. Read-only. Requires Campaigns view.
5393
5489
 
5394
5490
  - API: `GET /v1/build-sessions/by-campaign`
5395
5491
  - Generated from the API contract
@@ -5483,7 +5579,7 @@ kenso build-sessions feedback --data @body.json
5483
5579
 
5484
5580
  Return one build session.
5485
5581
 
5486
- A missing or cross-workspace id is `not_found`. Read-only. Requires `ReadAccess`.
5582
+ A missing or cross-workspace id is `not_found`. Read-only. Requires Campaigns view.
5487
5583
 
5488
5584
  - API: `GET /v1/build-sessions/{buildSessionId}`
5489
5585
  - Generated from the API contract
@@ -5502,7 +5598,7 @@ kenso build-sessions get --build-session-id <build-session-id>
5502
5598
 
5503
5599
  List agent memory for the current workspace.
5504
5600
 
5505
- Memory ids are not numeric, so this returns the matching rows in one window. Read-only. Requires `ReadAccess`.
5601
+ Memory ids are not numeric, so this returns the matching rows in one window. Read-only. Requires Campaigns view.
5506
5602
 
5507
5603
  - API: `GET /v1/build-sessions/memory`
5508
5604
  - Generated from the API contract
@@ -6045,7 +6141,7 @@ kenso calendars update --calendar-id <calendar-id> --data @body.json
6045
6141
 
6046
6142
  List a member's Call queue.
6047
6143
 
6048
- Open call tasks that have not expired, assigned to the member, and open call touches (planned, drafted, awaiting approval or failed) on the member's accounts or on accounts with no owner. Each item carries the person or contact's `name`, `title`, `company`, `phone` (the task's, else the person's primary phone, else the contact's), `timeZoneId` and `zoneSource` (account, phone, owner or default), `callableNow` and `opensAt` (when the call can be made: once it is due, inside business hours while Contact limits keep calls to them, and under the calls per person per day limit, else `limitReached` and the next day's window, or local midnight while any time is callable; a snoozed or future call opens at its due time), `reason`, `attempts` and `briefReady`. Callable first, then by `dueAt`, then id. Filters: `assignee` (`me`, the default, or a member id for a workspace admin, else `403`), `due` (`now`, `today` before the end of the local day in the caller's app time zone, else in `timezone`, or `all`, the default). `limit` 1-200 (default 50); `pagination.hasMore` says more exist and there is no cursor. `counts`: `open` and `callableNow` over the whole queue read (at most 500). Requires `ReadAccess` (accounts view).
6144
+ Open call tasks that have not expired, assigned to the member, and open call touches (planned, drafted, awaiting approval or failed) on the member's accounts or on accounts with no owner. Each item carries the person or contact's `name`, `title`, `company`, `phone` (the task's, else the person's primary phone, else the contact's), `timeZoneId` and `zoneSource` (account, phone, owner or default), `callableNow` and `opensAt` (when the call can be made: once it is due, inside business hours while Contact limits keep calls to them, and under the calls per person per day limit, else `limitReached` and the next day's window, or local midnight while any time is callable; a snoozed or future call opens at its due time), `reason`, `attempts` and `briefReady`. Callable first, then by `dueAt`, then id. Filters: `assignee` (`me`, the default, or a member id for a workspace admin, else `403`), `due` (`now`, `today` before the end of the local day in the caller's app time zone, else in `timezone`, or `all`, the default). `limit` 1-200 (default 50); `pagination.hasMore` says more exist and there is no cursor. `counts`: `open` and `callableNow` over the whole queue read (at most 500). Requires Accounts view.
6049
6145
 
6050
6146
  - Beta: needs the `calls` feature flag on the workspace.
6051
6147
  - API: `GET /v1/call-queue`
@@ -6115,7 +6211,7 @@ kenso call-sequences replace --campaign-id <campaign-id> --data @body.json
6115
6211
 
6116
6212
  Create a call task.
6117
6213
 
6118
- Body: `personId` or `contactId` (exactly one; a contact's task is also on its person when it has one), `dueAt` (default now, at most 90 days out, else `400 out_of_range`), `title` (1-200), `instructions`, `assigneeMemberId` (default the caller; a member of this workspace, else `400 invalid_member`) and `phone` (any format, stored E.164; a bad number is `400 invalid_phone`; default the person's primary phone when the call is made). The reason is `manual`, `accountId` the person's first account, `expiresAt` a week after `dueAt` (after now for a past `dueAt`). Writes `call.task_created` on the person's timeline. Returns `201`. When the person (else contact) already has an open call task, that task comes back with `200` and `existing: true`, also when another writer wins the race; one past its `expiresAt` is expired instead and a new task created. Under account scope `own`, an open task assigned to someone else is `409 conflict` with detail `call_task_exists` and no task id, and a contact whose person is not on the caller's accounts is `404`. Requires `WriteAccess` (accounts edit).
6214
+ Body: `personId` or `contactId` (exactly one; a contact's task is also on its person when it has one), `dueAt` (default now, at most 90 days out, else `400 out_of_range`), `title` (1-200), `instructions`, `assigneeMemberId` (default the caller; a member of this workspace, else `400 invalid_member`) and `phone` (any format, stored E.164; a bad number is `400 invalid_phone`; default the person's primary phone when the call is made). The reason is `manual`, `accountId` the person's first account, `expiresAt` a week after `dueAt` (after now for a past `dueAt`). Writes `call.task_created` on the person's timeline. Returns `201`. When the person (else contact) already has an open call task, that task comes back with `200` and `existing: true`, also when another writer wins the race; one past its `expiresAt` is expired instead and a new task created. Under account scope `own`, an open task assigned to someone else is `409 conflict` with detail `call_task_exists` and no task id, and a contact whose person is not on the caller's accounts is `404`. Requires Accounts edit.
6119
6215
 
6120
6216
  - Beta: needs the `calls` feature flag on the workspace.
6121
6217
  - API: `POST /v1/call-tasks`
@@ -6144,7 +6240,7 @@ kenso call-tasks create --data @body.json
6144
6240
 
6145
6241
  Delete a call task.
6146
6242
 
6147
- Soft delete: the task leaves every list and the Call queue. Returns `204`. Requires `WriteAccess`.
6243
+ Soft delete: the task leaves every list and the Call queue. Returns `204`. Requires Accounts edit.
6148
6244
 
6149
6245
  - Beta: needs the `calls` feature flag on the workspace.
6150
6246
  - API: `DELETE /v1/call-tasks/{callTaskId}`
@@ -6164,7 +6260,7 @@ kenso call-tasks delete --call-task-id <call-task-id>
6164
6260
 
6165
6261
  Return one call task.
6166
6262
 
6167
- A task of another workspace, a deleted one, or another member's under account scope `own` is `404`. Requires `ReadAccess`.
6263
+ A task of another workspace, a deleted one, or another member's under account scope `own` is `404`. Requires Accounts view.
6168
6264
 
6169
6265
  - Beta: needs the `calls` feature flag on the workspace.
6170
6266
  - API: `GET /v1/call-tasks/{callTaskId}`
@@ -6204,7 +6300,7 @@ kenso call-tasks get-brief --call-task-id <call-task-id>
6204
6300
 
6205
6301
  List call tasks.
6206
6302
 
6207
- Live tasks by `dueAt`, then id. Filters: `assignee` (`me` or a member id; blank is every member, and a member with account scope `own` always gets only their own, so naming another member is `403 permission_denied` with `scope: all`), `status` (open, done, skipped, expired; comma = OR), `due` (`now` due before now, `today` before the end of the local day, overdue included, `overdue` before the local day started), `timezone` (IANA, default UTC, for a caller with no app time zone; the app time zone sets the local day when set), `personId`, `accountId` and `campaignId`. `limit` 1-200 (default 50), `cursor` from `pagination.nextCursor`. Requires `ReadAccess` (accounts view).
6303
+ Live tasks by `dueAt`, then id. Filters: `assignee` (`me` or a member id; blank is every member, and a member with account scope `own` always gets only their own, so naming another member is `403 permission_denied` with `scope: all`), `status` (open, done, skipped, expired; comma = OR), `due` (`now` due before now, `today` before the end of the local day, overdue included, `overdue` before the local day started), `timezone` (IANA, default UTC, for a caller with no app time zone; the app time zone sets the local day when set), `personId`, `accountId` and `campaignId`. `limit` 1-200 (default 50), `cursor` from `pagination.nextCursor`. Requires Accounts view.
6208
6304
 
6209
6305
  - Beta: needs the `calls` feature flag on the workspace.
6210
6306
  - API: `GET /v1/call-tasks` (a list: `--limit`, `--cursor`, `--all`)
@@ -6234,7 +6330,7 @@ kenso call-tasks list --all
6234
6330
 
6235
6331
  Skip an open call task.
6236
6332
 
6237
- Body: `reason`, one of `no_phone`, `not_relevant` or `other`. The task becomes `skipped` with `skipReason` and `doneAt`, and leaves the Call queue; `call.task_skipped` goes on the person's timeline. Reopen it with `PATCH``status: open`. A task that is not open is `409 invalid_state`. A task another writer changed while the request ran is `409 conflict` with detail `call_task_changed`: read it again and retry. Requires `WriteAccess`.
6333
+ Body: `reason`, one of `no_phone`, `not_relevant` or `other`. The task becomes `skipped` with `skipReason` and `doneAt`, and leaves the Call queue; `call.task_skipped` goes on the person's timeline. Reopen it with `PATCH``status: open`. A task that is not open is `409 invalid_state`. A task another writer changed while the request ran is `409 conflict` with detail `call_task_changed`: read it again and retry. Requires Accounts edit.
6238
6334
 
6239
6335
  - Beta: needs the `calls` feature flag on the workspace.
6240
6336
  - API: `POST /v1/call-tasks/{callTaskId}/skip`
@@ -6258,7 +6354,7 @@ kenso call-tasks skip --call-task-id <call-task-id> --data @body.json
6258
6354
 
6259
6355
  Snooze an open call task.
6260
6356
 
6261
- Body, optional: `until` (in the future, at most 90 days out). Without it the task moves to the next day's calling window in its zone (08:00 local the next weekday while Contact limits keep calls to business hours, else a day from now). `expiresAt` moves with it and `snoozeCount` goes up by one. A task that is not open is `409 invalid_state`. A task another writer changed while the request ran is `409 conflict` with detail `call_task_changed`: read it again and retry. Requires `WriteAccess`.
6357
+ Body, optional: `until` (in the future, at most 90 days out). Without it the task moves to the next day's calling window in its zone (08:00 local the next weekday while Contact limits keep calls to business hours, else a day from now). `expiresAt` moves with it and `snoozeCount` goes up by one. A task that is not open is `409 invalid_state`. A task another writer changed while the request ran is `409 conflict` with detail `call_task_changed`: read it again and retry. Requires Accounts edit.
6262
6358
 
6263
6359
  - Beta: needs the `calls` feature flag on the workspace.
6264
6360
  - API: `POST /v1/call-tasks/{callTaskId}/snooze`
@@ -6282,7 +6378,7 @@ kenso call-tasks snooze --call-task-id <call-task-id> --data @body.json
6282
6378
 
6283
6379
  Update a call task. Absent fields stay unchanged.
6284
6380
 
6285
- Fields of an open task: `dueAt` (at most 90 days out; moves `expiresAt` with it), `title`, `instructions`, `assigneeMemberId` (a member of this workspace, not null) and `phone` (null clears; a bad number is `400 invalid_phone`). `status` can only be `open`: it reopens a skipped or expired task, clears its skip reason and done time and keeps it in the queue for another week; a done task stays done (`409 invalid_state`). When the person (else contact) already has another open task, the reopen is `409 conflict` with detail `call_task_exists` and that task's `callTaskId`; one past its `expiresAt` is expired instead. A task another writer changed while the request ran is `409 conflict` with detail `call_task_changed`: read it again and retry. Skip with `POST /v1/call-tasks/{callTaskId}/skip`. Requires `WriteAccess`.
6381
+ Fields of an open task: `dueAt` (at most 90 days out; moves `expiresAt` with it), `title`, `instructions`, `assigneeMemberId` (a member of this workspace, not null) and `phone` (null clears; a bad number is `400 invalid_phone`). `status` can only be `open`: it reopens a skipped or expired task, clears its skip reason and done time and keeps it in the queue for another week; a done task stays done (`409 invalid_state`). When the person (else contact) already has another open task, the reopen is `409 conflict` with detail `call_task_exists` and that task's `callTaskId`; one past its `expiresAt` is expired instead. A task another writer changed while the request ran is `409 conflict` with detail `call_task_changed`: read it again and retry. Skip with `POST /v1/call-tasks/{callTaskId}/skip`. Requires Accounts edit.
6286
6382
 
6287
6383
  - Beta: needs the `calls` feature flag on the workspace.
6288
6384
  - API: `PATCH /v1/call-tasks/{callTaskId}`
@@ -6456,7 +6552,7 @@ kenso calling numbers-buy --data @body.json
6456
6552
 
6457
6553
  List the workspace's phone numbers.
6458
6554
 
6459
- Every number not released, newest first, at most five. `unpaid` is true when its monthly credits could not be charged. Requires `ReadAccess`.
6555
+ Every number not released, newest first, at most five. `unpaid` is true when its monthly credits could not be charged. Open to any workspace member.
6460
6556
 
6461
6557
  - Beta: needs the `calling` feature flag on the workspace.
6462
6558
  - API: `GET /v1/calling/numbers`
@@ -6518,7 +6614,7 @@ kenso calling numbers-update --number-id <number-id> --data @body.json
6518
6614
 
6519
6615
  Return what a call to a number costs.
6520
6616
 
6521
- `to` is the number to call. `millicreditsPerMinute` is the destination rate plus the rep's leg, and `recordingMillicreditsPerMinute` is added when the call is recorded. A destination Kenso has no price for yet is `priced: false` with null rates; a value that is not a phone number is 400 `invalid_phone`. Requires `ReadAccess`.
6617
+ `to` is the number to call. `millicreditsPerMinute` is the destination rate plus the rep's leg, and `recordingMillicreditsPerMinute` is added when the call is recorded. A destination Kenso has no price for yet is `priced: false` with null rates; a value that is not a phone number is 400 `invalid_phone`. Open to any workspace member.
6522
6618
 
6523
6619
  - Beta: needs the `calling` feature flag on the workspace.
6524
6620
  - API: `GET /v1/calling/rates`
@@ -6538,7 +6634,7 @@ kenso calling rates-get
6538
6634
 
6539
6635
  Return the workspace's calling settings.
6540
6636
 
6541
- A workspace that never saved them gets the defaults and `updatedAt` null. Requires `ReadAccess`.
6637
+ A workspace that never saved them gets the defaults and `updatedAt` null. Open to any workspace member.
6542
6638
 
6543
6639
  - Beta: needs the `calling` feature flag on the workspace.
6544
6640
  - API: `GET /v1/calling/settings`
@@ -6579,7 +6675,7 @@ kenso calling settings-update --data @body.json
6579
6675
 
6580
6676
  Return the workspace's monthly calling spend limit.
6581
6677
 
6582
- `monthlyLimitMillicredits` null is no limit; a workspace that never set it reads 2,000,000 (2,000 credits) with `isDefault: true`. `spentThisMonthMillicredits` is call minutes and recordings charged this UTC calendar month plus open call holds, the same spend the cap counts; number fees are not in it. Separate from `/v1/spend-limits`. Requires `ReadAccess`.
6678
+ `monthlyLimitMillicredits` null is no limit; a workspace that never set it reads 2,000,000 (2,000 credits) with `isDefault: true`. `spentThisMonthMillicredits` is call minutes and recordings charged this UTC calendar month plus open call holds, the same spend the cap counts; number fees are not in it. Separate from `/v1/spend-limits`. Open to any workspace member.
6583
6679
 
6584
6680
  - Beta: needs the `calling` feature flag on the workspace.
6585
6681
  - API: `GET /v1/calling/spend-limit`
@@ -6682,7 +6778,7 @@ kenso calling voicemails-delete --voicemail-id <voicemail-id>
6682
6778
 
6683
6779
  List your voicemail greetings.
6684
6780
 
6685
- Your live greetings, oldest first: the first is the one an `auto` drop plays. `audioUrl` is a 5-minute link to listen to it, absent while Kenso's audio store is not set up. Requires `ReadAccess`.
6781
+ Your live greetings, oldest first: the first is the one an `auto` drop plays. `audioUrl` is a 5-minute link to listen to it, absent while Kenso's audio store is not set up. Open to any workspace member.
6686
6782
 
6687
6783
  - Beta: needs the `calling` feature flag on the workspace.
6688
6784
  - API: `GET /v1/calling/voicemails`
@@ -6703,7 +6799,7 @@ kenso calling voicemails-list
6703
6799
 
6704
6800
  Log a call.
6705
6801
 
6706
- Body: `personId`, `contactId` or `phone` (a phone no person or contact holds is `400 person_not_found`), `disposition` (required), `direction`, `startedAt` (default now), `endedAt` or `durationSeconds`, `notes`, `nextStep`, `followUpAt`, `callTaskId` or `touchId`, `recordingUrl` (https), `externalId` and `askedNotToBeCalled`. An API key logs source `api`, anyone else `manual`; the caller is the call's member. `askedNotToBeCalled: true` opts the person out of calls before the call is written (a call with no person is `400`; if the opt-out fails, nothing is logged). A wrong_number disposition never opts out. The disposition closes or retries the call task, completes the touch, and moves the lead (connected: working; meeting_booked: qualified, the account to meeting_booked and the campaign stopped; not_interested: disqualified and the campaign stopped; wrong_number: the number marked invalid and a phone find queued). `followUpAt` queues a follow-up call task. Writes `call.logged` on the person's timeline. Returns `201`; the same `externalId` from the same caller updates that call and returns `200`. Requires `WriteAccess` (accounts edit).
6802
+ Body: `personId`, `contactId` or `phone` (a phone no person or contact holds is `400 person_not_found`), `disposition` (required), `direction`, `startedAt` (default now), `endedAt` or `durationSeconds`, `notes`, `nextStep`, `followUpAt`, `callTaskId` or `touchId`, `recordingUrl` (https), `externalId` and `askedNotToBeCalled`. An API key logs source `api`, anyone else `manual`; the caller is the call's member. `askedNotToBeCalled: true` opts the person out of calls before the call is written (a call with no person is `400`; if the opt-out fails, nothing is logged). A wrong_number disposition never opts out. The disposition closes or retries the call task, completes the touch, and moves the lead (connected: working; meeting_booked: qualified, the account to meeting_booked and the campaign stopped; not_interested: disqualified and the campaign stopped; wrong_number: the number marked invalid and a phone find queued). `followUpAt` queues a follow-up call task. Writes `call.logged` on the person's timeline. Returns `201`; the same `externalId` from the same caller updates that call and returns `200`. Requires Accounts edit.
6707
6803
 
6708
6804
  - Beta: needs the `calls` feature flag on the workspace.
6709
6805
  - API: `POST /v1/calls`
@@ -6742,7 +6838,7 @@ kenso calls create --data @body.json
6742
6838
 
6743
6839
  Dial a call task, a call touch, a person or a number.
6744
6840
 
6745
- Body: one of `callTaskId`, `touchId` (an account touch on the call channel; its stakeholder's person is called), `personId` or `phone`, and `force`. The number is the task's, else the person's phone, else the task contact's. A person who asked not to be called is `409 opted_out`, and `force` does not override it. Outside the person's calling hours (the workspace's business hours in their zone) it is `409 outside_calling_hours` with `opensAt` in the error detail; at the workspace's calls per person per day it is `409 limit_reached` unless `force` is true. A connected dialer the caller is linked on places the call (`status: dialing`) and Kenso logs it at once: `callId`, with the task or touch it works and no disposition. `PATCH /v1/calls/{callId}` saves its outcome (closing the task or completing the touch) and the dialer's events fill it in, so one dial is one call. Otherwise `status: tel` with `telHref` and no call is written. Kenso with no number for the person is `409 no_phone`. Requires `WriteAccess` (accounts edit).
6841
+ Body: one of `callTaskId`, `touchId` (an account touch on the call channel; its stakeholder's person is called), `personId` or `phone`, and `force`. The number is the task's, else the person's phone, else the task contact's. A person who asked not to be called is `409 opted_out`, and `force` does not override it. Outside the person's calling hours (the workspace's business hours in their zone) it is `409 outside_calling_hours` with `opensAt` in the error detail; at the workspace's calls per person per day it is `409 limit_reached` unless `force` is true. A connected dialer the caller is linked on places the call (`status: dialing`) and Kenso logs it at once: `callId`, with the task or touch it works and no disposition. `PATCH /v1/calls/{callId}` saves its outcome (closing the task or completing the touch) and the dialer's events fill it in, so one dial is one call. Otherwise `status: tel` with `telHref` and no call is written. Kenso with no number for the person is `409 no_phone`. Requires Accounts edit.
6746
6842
 
6747
6843
  - Beta: needs the `calls` feature flag on the workspace.
6748
6844
  - API: `POST /v1/calls/dial`
@@ -6770,7 +6866,7 @@ kenso calls dial --data @body.json
6770
6866
 
6771
6867
  Get a call.
6772
6868
 
6773
- The call with its transcript. Another workspace's or a deleted call is 404. Requires `ReadAccess` (accounts view).
6869
+ The call with its transcript. Another workspace's or a deleted call is 404. Requires Accounts view.
6774
6870
 
6775
6871
  - Beta: needs the `calls` feature flag on the workspace.
6776
6872
  - API: `GET /v1/calls/{callId}`
@@ -6790,7 +6886,7 @@ kenso calls get --call-id <call-id>
6790
6886
 
6791
6887
  List calls.
6792
6888
 
6793
- Live calls, newest first (`startedAt`, then id). Filters: `personId`, `accountId`, `memberId` (who made or took the call), `campaignId`, `from` (started at or after) and `to` (started before), `disposition` (comma = OR). `limit` 1-200 (default 50), `cursor` from `pagination.nextCursor`. The list leaves out transcripts. Requires `ReadAccess` (accounts view).
6889
+ Live calls, newest first (`startedAt`, then id). Filters: `personId`, `accountId`, `memberId` (who made or took the call), `campaignId`, `from` (started at or after) and `to` (started before), `disposition` (comma = OR). `limit` 1-200 (default 50), `cursor` from `pagination.nextCursor`. The list leaves out transcripts. Requires Accounts view.
6794
6890
 
6795
6891
  - Beta: needs the `calls` feature flag on the workspace.
6796
6892
  - API: `GET /v1/calls` (a list: `--limit`, `--cursor`, `--all`)
@@ -6820,7 +6916,7 @@ kenso calls list --all
6820
6916
 
6821
6917
  Open a call's recording.
6822
6918
 
6823
- Redirects (302) to the recording: a fresh link from the dialer that logged the call while it is still connected, else the https link the call was logged with. No recording is 404. Requires `ReadAccess` (accounts view).
6919
+ Redirects (302) to the recording: a fresh link from the dialer that logged the call while it is still connected, else the https link the call was logged with. No recording is 404. Requires Accounts view.
6824
6920
 
6825
6921
  - Beta: needs the `calls` feature flag on the workspace.
6826
6922
  - API: `GET /v1/calls/{callId}/recording`
@@ -6840,7 +6936,7 @@ kenso calls recording --call-id <call-id>
6840
6936
 
6841
6937
  Update a call.
6842
6938
 
6843
- Body: `disposition` (cannot be null), `notes`, `nextStep`, `followUpAt`; absent fields stay, null clears. The first disposition a call gets runs its effects as on create; changing it later writes `call.updated` only. A new `followUpAt` queues a follow-up call task. `askedNotToBeCalled: true` opts the call's person out of calls (a call with no person is `400`). Requires `WriteAccess` (accounts edit).
6939
+ Body: `disposition` (cannot be null), `notes`, `nextStep`, `followUpAt`; absent fields stay, null clears. The first disposition a call gets runs its effects as on create; changing it later writes `call.updated` only. A new `followUpAt` queues a follow-up call task. `askedNotToBeCalled: true` opts the call's person out of calls (a call with no person is `400`). Requires Accounts edit.
6844
6940
 
6845
6941
  - Beta: needs the `calls` feature flag on the workspace.
6846
6942
  - API: `PATCH /v1/calls/{callId}`
@@ -7578,7 +7674,7 @@ kenso campaign-video-variables update --campaign-id <campaign-id> --variable-key
7578
7674
 
7579
7675
  Archive a campaign.
7580
7676
 
7581
- Idempotent for an already-archived campaign. A campaign that is still sending returns `409 invalid_state`. Requires `WriteAccess`.
7677
+ Idempotent for an already-archived campaign. A campaign that is still sending returns `409 invalid_state`. Requires Campaigns edit.
7582
7678
 
7583
7679
  - API: `POST /v1/campaigns/{campaignId}/archive`
7584
7680
  - Generated from the API contract
@@ -7682,7 +7778,7 @@ kenso campaigns asset-template-put --campaign-id <campaign-id> --data @body.json
7682
7778
 
7683
7779
  Create a campaign in the current workspace.
7684
7780
 
7685
- Returns `201` and `Location`. Requires `ReadAccess`, matching the legacy create. Reserves the `campaignCreate` abuse quota. A sales campaign starts with `stopCompanyOnReply` on. Optional `kind` is `sales` (default) or `recruiting`. A recruiting campaign needs the `recruiting` flag for the workspace, else `400 validation_failed` with detail code `feature_disabled`; it starts with `stopCompanyOnReply`, `groupSameCompanySends` and same-company Cc off, and as a Draft, so its AI build can start. Optional `roleId` names a live role of the workspace (else `404 not_found`) and is only for a recruiting campaign (else `400` with detail code `not_recruiting`).
7781
+ Returns `201` and `Location`. Requires Campaigns edit. Reserves the `campaignCreate` abuse quota. A sales campaign starts with `stopCompanyOnReply` on. Optional `kind` is `sales` (default) or `recruiting`. A recruiting campaign needs the `recruiting` flag for the workspace, else `400 validation_failed` with detail code `feature_disabled`; it starts with `stopCompanyOnReply`, `groupSameCompanySends` and same-company Cc off, and as a Draft, so its AI build can start. Optional `roleId` names a live role of the workspace (else `404 not_found`) and is only for a recruiting campaign (else `400` with detail code `not_recruiting`).
7686
7782
 
7687
7783
  - API: `POST /v1/campaigns`
7688
7784
  - Generated from the API contract
@@ -7707,7 +7803,7 @@ kenso campaigns create --data @body.json
7707
7803
 
7708
7804
  Queue lead enrichment for a campaign.
7709
7805
 
7710
- Replaces the legacy campaign enrichment trigger. The request finishes when Search accepts the batches and returns an `enrichment` job. `minConfidence` defaults to 0.5. Requires `WriteAccess`. A campaign outside the workspace is `not_found` before the run.
7806
+ Replaces the legacy campaign enrichment trigger. The request finishes when Search accepts the batches and returns an `enrichment` job. `minConfidence` defaults to 0.5. Requires Campaigns edit. A campaign outside the workspace is `not_found` before the run.
7711
7807
 
7712
7808
  - API: `POST /v1/campaigns/{campaignId}/enrichment-runs`
7713
7809
  - Generated from the API contract
@@ -7730,7 +7826,7 @@ kenso campaigns create-enrichment-run --campaign-id <campaign-id> --data @body.j
7730
7826
 
7731
7827
  Soft-delete a campaign.
7732
7828
 
7733
- Requires `ReadAccess`, matching the legacy delete. Returns `204`.
7829
+ Requires Campaigns edit. Returns `204`.
7734
7830
 
7735
7831
  - API: `DELETE /v1/campaigns/{campaignId}`
7736
7832
  - Generated from the API contract
@@ -7749,7 +7845,7 @@ kenso campaigns delete --campaign-id <campaign-id>
7749
7845
 
7750
7846
  Duplicate a campaign inside the current workspace.
7751
7847
 
7752
- `includeContacts` copies contacts. The copy stays in this workspace. Returns `201` and `Location`. Requires `WriteAccess`. Reserves the campaign-duplicate abuse quota.
7848
+ `includeContacts` copies contacts. The copy stays in this workspace. Returns `201` and `Location`. Requires Campaigns edit. Reserves the campaign-duplicate abuse quota.
7753
7849
 
7754
7850
  - API: `POST /v1/campaigns/{campaignId}/duplicate`
7755
7851
  - Generated from the API contract
@@ -7774,7 +7870,7 @@ kenso campaigns duplicate --campaign-id <campaign-id> --data @body.json
7774
7870
 
7775
7871
  Return one campaign in the current workspace.
7776
7872
 
7777
- A missing or cross-workspace id is `not_found`. Read-only. Requires `ReadAccess`.
7873
+ A missing or cross-workspace id is `not_found`. Read-only. Requires Campaigns view.
7778
7874
 
7779
7875
  - API: `GET /v1/campaigns/{campaignId}`
7780
7876
  - Generated from the API contract
@@ -7833,7 +7929,7 @@ kenso campaigns get-autopilot --campaign-id <campaign-id>
7833
7929
 
7834
7930
  Report whether a campaign can launch and how long sending should take.
7835
7931
 
7836
- Replaces `GET /v1/campaigns/{id}/can-launch` and `GET /v1/campaigns/{id}/sending-estimate`. A campaign outside the workspace is `not_found` before either read. Requires `ReadAccess`. `warnings` never change `canLaunch`; `localization_incomplete` (one per enabled language of an `auto` campaign) names the steps with no approved translation and whether those contacts get the source text or are held. `approval_required` means the client was asked and launch answers 409 `approval_required` until they approve; its `kinds` lists what waits (`copy`, `lead_list`, ...). The warning `approval_will_be_requested` names what nobody asked about yet: launch asks the client, then refuses until they approve.
7932
+ Replaces `GET /v1/campaigns/{id}/can-launch` and `GET /v1/campaigns/{id}/sending-estimate`. A campaign outside the workspace is `not_found` before either read. Requires Campaigns view. `warnings` never change `canLaunch`; `localization_incomplete` (one per enabled language of an `auto` campaign) names the steps with no approved translation and whether those contacts get the source text or are held. `approval_required` means the client was asked and launch answers 409 `approval_required` until they approve; its `kinds` lists what waits (`copy`, `lead_list`, ...). The warning `approval_will_be_requested` names what nobody asked about yet: launch asks the client, then refuses until they approve.
7837
7933
 
7838
7934
  - API: `GET /v1/campaigns/{campaignId}/launch-readiness`
7839
7935
  - Generated from the API contract
@@ -7852,7 +7948,7 @@ kenso campaigns get-launch-readiness --campaign-id <campaign-id>
7852
7948
 
7853
7949
  Read a campaign's LinkedIn settings.
7854
7950
 
7855
- Replaces `GET /v1/campaigns/{id}/linkedin-config`. A campaign outside the workspace is `not_found`. Requires `ReadAccess`.
7951
+ Replaces `GET /v1/campaigns/{id}/linkedin-config`. A campaign outside the workspace is `not_found`. Requires Campaigns view.
7856
7952
 
7857
7953
  - API: `GET /v1/campaigns/{campaignId}/linkedin-settings`
7858
7954
  - Generated from the API contract
@@ -7871,7 +7967,7 @@ kenso campaigns get-linkedin-settings --campaign-id <campaign-id>
7871
7967
 
7872
7968
  Count a campaign's contacts by email security gateway and mailbox host.
7873
7969
 
7874
- Each live contact is judged by its first Safe or Verified email, the one the lead list shows. `gateways` lists every active security gateway and `mailboxProviders` every active mailbox host and forwarder, 0 included, most contacts first. `pendingCount` counts contacts whose domain is not checked for a gateway yet. Counts cover at most 100,000 contacts (`isPartial`). Read-only. A campaign outside the workspace, or a workspace without the `security_gateways` feature, is `not_found`. Requires `ReadAccess`.
7970
+ Each live contact is judged by its first Safe or Verified email, the one the lead list shows. `gateways` lists every active security gateway and `mailboxProviders` every active mailbox host and forwarder, 0 included, most contacts first. `pendingCount` counts contacts whose domain is not checked for a gateway yet. Counts cover at most 100,000 contacts (`isPartial`). Read-only. A campaign outside the workspace, or a workspace without the `security_gateways` feature, is `not_found`. Requires Campaigns view.
7875
7971
 
7876
7972
  - Beta: needs the `security_gateways` feature flag on the workspace.
7877
7973
  - API: `GET /v1/campaigns/{campaignId}/mail-providers`
@@ -7911,7 +8007,7 @@ kenso campaigns get-processing-status --campaign-id <campaign-id>
7911
8007
 
7912
8008
  Read whether lead scoring is enabled for a campaign.
7913
8009
 
7914
- Replaces `POST /v1/campaigns/{id}/scoring` for reads. A campaign outside the workspace is `not_found`. Requires `ReadAccess`.
8010
+ Replaces `POST /v1/campaigns/{id}/scoring` for reads. A campaign outside the workspace is `not_found`. Requires Campaigns view.
7915
8011
 
7916
8012
  - API: `GET /v1/campaigns/{campaignId}/scoring`
7917
8013
  - Generated from the API contract
@@ -7930,7 +8026,7 @@ kenso campaigns get-scoring --campaign-id <campaign-id>
7930
8026
 
7931
8027
  Return a campaign's sender routing.
7932
8028
 
7933
- DEV-5225. `allInboxes` is the campaign's inbox mode. Each rule sends the first email to prospects on its mailbox provider or security gateway only from its domains and inboxes; a gateway rule wins over a mailbox provider rule, and prospects no rule matches go from the inboxes no rule claims (`everyoneElse`). Per rule and for Everyone else: the campaign's inboxes that can send now (Kenso-sendable, not paused), their summed daily limits, and the leads that can still get an email and match now (at most 100,000 leads read; a lead whose provider is still being detected is in no group). `warnings`: `rule_no_inbox`, `everyone_else_no_inbox`, `rule_slow` (over 5 days at the rule's capacity). Read-only. Needs the workspace's `sender_routing` flag, else `400 validation_failed` with detail code `feature_disabled`. Requires `ReadAccess`. A campaign outside the workspace is `not_found`.
8029
+ DEV-5225. `allInboxes` is the campaign's inbox mode. Each rule sends the first email to prospects on its mailbox provider or security gateway only from its domains and inboxes; a gateway rule wins over a mailbox provider rule, and prospects no rule matches go from the inboxes no rule claims (`everyoneElse`). Per rule and for Everyone else: the campaign's inboxes that can send now (Kenso-sendable, not paused), their summed daily limits, and the leads that can still get an email and match now (at most 100,000 leads read; a lead whose provider is still being detected is in no group). `warnings`: `rule_no_inbox`, `everyone_else_no_inbox`, `rule_slow` (over 5 days at the rule's capacity). Read-only. Needs the workspace's `sender_routing` flag, else `400 validation_failed` with detail code `feature_disabled`. Requires Campaigns view. A campaign outside the workspace is `not_found`.
7934
8030
 
7935
8031
  - API: `GET /v1/campaigns/{campaignId}/sender-routing`
7936
8032
  - Generated from the API contract
@@ -7949,7 +8045,7 @@ kenso campaigns get-sender-routing --campaign-id <campaign-id>
7949
8045
 
7950
8046
  Return one campaign's weekly report.
7951
8047
 
7952
- `weekOf` is any date (`YYYY-MM-DD`, from 2000-01-01 to one year from today) in the wanted Monday-to-Sunday week. `timeZone` is an IANA name; without it the campaign's sending time zone is used, else UTC. The week's dates are picked in that time zone; its numbers and `changed` are counted on those UTC days, like the Analytics tab. Without `weekOf` the report covers the latest week that has ended both there and in UTC. `week` and `toDate` are email counts from campaign performance. `toDate` and `segments` run from the campaign's creation (at most 366 days back) to the end of the week. `changed` and `next` are sentences built from data, not AI. `next` is as of the request, in the campaign's sending time zone, and is empty for a week older than the default week. A missing or cross-workspace campaign is `not_found`. Read-only. Requires `ReadAccess`.
8048
+ `weekOf` is any date (`YYYY-MM-DD`, from 2000-01-01 to one year from today) in the wanted Monday-to-Sunday week. `timeZone` is an IANA name; without it the campaign's sending time zone is used, else UTC. The week's dates are picked in that time zone; its numbers and `changed` are counted on those UTC days, like the Analytics tab. Without `weekOf` the report covers the latest week that has ended both there and in UTC. `week` and `toDate` are email counts from campaign performance. `toDate` and `segments` run from the campaign's creation (at most 366 days back) to the end of the week. `changed` and `next` are sentences built from data, not AI. `next` is as of the request, in the campaign's sending time zone, and is empty for a week older than the default week. A missing or cross-workspace campaign is `not_found`. Read-only. Requires Campaigns view.
7953
8049
 
7954
8050
  - API: `GET /v1/campaigns/{campaignId}/weekly-report`
7955
8051
  - Generated from the API contract
@@ -7970,7 +8066,7 @@ kenso campaigns get-weekly-report --campaign-id <campaign-id>
7970
8066
 
7971
8067
  Report whether a campaign's workflows can start.
7972
8068
 
7973
- Replaces `startWorkflowReadiness` on the legacy `GET /v1/campaigns/{id}`. Runs the same checks that gate starting enrichment, verification, and AI workflows (imported contacts or an active signal, contact limit, name, schedule, email sequence, AI variables) and returns each one with `isPassed`. Sending launch readiness is `GET .../launch-readiness`. A campaign outside the workspace is `not_found`. Read-only. Requires `ReadAccess`.
8069
+ Replaces `startWorkflowReadiness` on the legacy `GET /v1/campaigns/{id}`. Runs the same checks that gate starting enrichment, verification, and AI workflows (imported contacts or an active signal, contact limit, name, schedule, email sequence, AI variables) and returns each one with `isPassed`. Sending launch readiness is `GET .../launch-readiness`. A campaign outside the workspace is `not_found`. Read-only. Requires Campaigns view.
7974
8070
 
7975
8071
  - API: `GET /v1/campaigns/{campaignId}/workflow-readiness`
7976
8072
  - Generated from the API contract
@@ -7989,7 +8085,7 @@ kenso campaigns get-workflow-readiness --campaign-id <campaign-id>
7989
8085
 
7990
8086
  Launch sending for a campaign.
7991
8087
 
7992
- Requires `WriteAccess`. Returns the campaign after the launch command confirms. While the workspace's data is scheduled for deletion or being deleted, returns `409 invalid_state`.
8088
+ Requires Campaigns edit and the `launch` permission. Returns the campaign after the launch command confirms. While the workspace's data is scheduled for deletion or being deleted, returns `409 invalid_state`.
7993
8089
 
7994
8090
  - API: `POST /v1/campaigns/{campaignId}/launch`
7995
8091
  - Generated from the API contract
@@ -8008,7 +8104,7 @@ kenso campaigns launch --campaign-id <campaign-id>
8008
8104
 
8009
8105
  List campaigns in the current workspace.
8010
8106
 
8011
- Includes archived campaigns and excludes approval-test drafts. Optional `status` is a comma-separated OR filter. `q` is a case-insensitive substring of the name, or of the owner's first name, last name, full name, or email. Optional `kind` is `sales` or `recruiting`. Each item carries `leadCount`, from one cached count of the workspace. It usually lags new or removed leads by under 15 minutes; on a very large workspace the first list after a quiet period can show an older count, which the next list corrects. A campaign that count has not seen is counted live, and a single-campaign read (`GET /v1/campaigns/{campaignId}`) counts live for a current number. `leadCount` is omitted when that count does not finish in time on a very large workspace. Each item carries `linkedInOnly`, true for a LinkedIn-only campaign, read once for the page. It is only on this list: it is omitted on a single-campaign read and when the read fails. Ordered by id. Read-only. Requires `ReadAccess`.
8107
+ Includes archived campaigns and excludes approval-test drafts. Optional `status` is a comma-separated OR filter. `q` is a case-insensitive substring of the name, or of the owner's first name, last name, full name, or email. Optional `kind` is `sales` or `recruiting`. Each item carries `leadCount`, from one cached count of the workspace. It usually lags new or removed leads by under 15 minutes; on a very large workspace the first list after a quiet period can show an older count, which the next list corrects. A campaign that count has not seen is counted live, and a single-campaign read (`GET /v1/campaigns/{campaignId}`) counts live for a current number. `leadCount` is omitted when that count does not finish in time on a very large workspace. Each item carries `linkedInOnly`, true for a LinkedIn-only campaign, read once for the page. It is only on this list: it is omitted on a single-campaign read and when the read fails. Ordered by id. Read-only. Requires Campaigns view.
8012
8108
 
8013
8109
  - API: `GET /v1/campaigns` (a list: `--limit`, `--cursor`, `--all`)
8014
8110
  - Generated from the API contract
@@ -8033,7 +8129,7 @@ kenso campaigns list --all
8033
8129
 
8034
8130
  List the AI workflows attached to a campaign.
8035
8131
 
8036
- Ids: 1 qualification, 2 personalization, 3 rewriting, 4 segmentation, 6 scoring, 8 line check. Read-only. Requires `ReadAccess`. A campaign outside the workspace is `not_found`.
8132
+ Ids: 1 qualification, 2 personalization, 3 rewriting, 4 segmentation, 6 scoring, 8 line check. Read-only. Requires Campaigns view. A campaign outside the workspace is `not_found`.
8037
8133
 
8038
8134
  - API: `GET /v1/campaigns/{campaignId}/ai-workflows`
8039
8135
  - Generated from the API contract
@@ -8052,7 +8148,7 @@ kenso campaigns list-ai-workflows --campaign-id <campaign-id>
8052
8148
 
8053
8149
  List inboxes assigned to a campaign.
8054
8150
 
8055
- Requires `ReadAccess`. A campaign outside the workspace is `not_found`.
8151
+ Requires Campaigns view. A campaign outside the workspace is `not_found`.
8056
8152
 
8057
8153
  - API: `GET /v1/campaigns/{campaignId}/inboxes`
8058
8154
  - Generated from the API contract
@@ -8071,7 +8167,7 @@ kenso campaigns list-inboxes --campaign-id <campaign-id>
8071
8167
 
8072
8168
  List LinkedIn accounts assigned to a campaign.
8073
8169
 
8074
- Requires `ReadAccess`. A campaign outside the workspace is `not_found`.
8170
+ Requires Campaigns view. A campaign outside the workspace is `not_found`.
8075
8171
 
8076
8172
  - API: `GET /v1/campaigns/{campaignId}/linkedin-accounts`
8077
8173
  - Generated from the API contract
@@ -8090,7 +8186,7 @@ kenso campaigns list-linkedin-accounts --campaign-id <campaign-id>
8090
8186
 
8091
8187
  Count a campaign's leads by mailbox provider and security gateway.
8092
8188
 
8093
- DEV-5225. Every active mailbox provider and security gateway with the campaign's leads behind it (0 included), most leads first: the leads that can still get an email, by the domain of the address they are sent to. A lead counts once under its mailbox provider and once under its security gateway. `pendingCount`: provider still being detected. `noProviderCount`: no listed provider. Counts cover at most 100,000 leads (`isPartial`). Read-only. Needs the `sender_routing` flag (`feature_disabled`). Requires `ReadAccess`. A campaign outside the workspace is `not_found`.
8189
+ DEV-5225. Every active mailbox provider and security gateway with the campaign's leads behind it (0 included), most leads first: the leads that can still get an email, by the domain of the address they are sent to. A lead counts once under its mailbox provider and once under its security gateway. `pendingCount`: provider still being detected. `noProviderCount`: no listed provider. Counts cover at most 100,000 leads (`isPartial`). Read-only. Needs the `sender_routing` flag (`feature_disabled`). Requires Campaigns view. A campaign outside the workspace is `not_found`.
8094
8190
 
8095
8191
  - API: `GET /v1/campaigns/{campaignId}/recipient-providers`
8096
8192
  - Generated from the API contract
@@ -8109,7 +8205,7 @@ kenso campaigns list-recipient-providers --campaign-id <campaign-id>
8109
8205
 
8110
8206
  Pause sending, processing, and AI workflows together.
8111
8207
 
8112
- Idempotent. A campaign that is already paused returns `200` with the unchanged resource. A subsystem that is not running is left as it is. If any subsystem fails, the action returns that error and the caller can retry. A campaign that is still launching returns `409 invalid_state`. Requires `WriteAccess`.
8208
+ Idempotent. A campaign that is already paused returns `200` with the unchanged resource. A subsystem that is not running is left as it is. If any subsystem fails, the action returns that error and the caller can retry. A campaign that is still launching returns `409 invalid_state`. Requires Campaigns edit and the `launch` permission.
8113
8209
 
8114
8210
  - API: `POST /v1/campaigns/{campaignId}/pause`
8115
8211
  - Generated from the API contract
@@ -8128,7 +8224,7 @@ kenso campaigns pause --campaign-id <campaign-id>
8128
8224
 
8129
8225
  Replace the AI workflows attached to a campaign.
8130
8226
 
8131
- Full replace: send every workflow id the campaign should run in `aiWorkflowIds`. Ids not listed are detached, and an empty list detaches all of them. Read the current set first to add one workflow. Ids: 1 qualification, 2 personalization, 3 rewriting, 4 segmentation, 6 scoring, 8 line check. Rewriting runs only when the campaign has rewriting prompts. Line check (8) grades personalization lines before they are saved and holds a contact whose lines fail; a campaign gets it with personalization by default, and detaching it is the opt-out. This route does not add it: keep 8 in the list to keep it on. While the campaign is scheduled, launching, or sending, 1, 2, and 4 must stay as they are and 3 may only be added; any other change to them is `invalid_state` (409) because they gate which contacts may send, so pause first. 6 and 8 can change at any time. No other campaign field changes. Requires `WriteAccess`. A campaign outside the workspace is `not_found` before the write.
8227
+ Full replace: send every workflow id the campaign should run in `aiWorkflowIds`. Ids not listed are detached, and an empty list detaches all of them. Read the current set first to add one workflow. Ids: 1 qualification, 2 personalization, 3 rewriting, 4 segmentation, 6 scoring, 8 line check. Rewriting runs only when the campaign has rewriting prompts. Line check (8) grades personalization lines before they are saved and holds a contact whose lines fail; a campaign gets it with personalization by default, and detaching it is the opt-out. This route does not add it: keep 8 in the list to keep it on. While the campaign is scheduled, launching, or sending, 1, 2, and 4 must stay as they are and 3 may only be added; any other change to them is `invalid_state` (409) because they gate which contacts may send, so pause first. 6 and 8 can change at any time. No other campaign field changes. Requires Campaigns edit. A campaign outside the workspace is `not_found` before the write.
8132
8228
 
8133
8229
  - API: `PUT /v1/campaigns/{campaignId}/ai-workflows`
8134
8230
  - Generated from the API contract
@@ -8179,7 +8275,7 @@ kenso campaigns replace-autopilot --campaign-id <campaign-id> --data @body.json
8179
8275
 
8180
8276
  Replace the inboxes assigned to a campaign.
8181
8277
 
8182
- Full replace. Send `inboxIds`, or `assignAllActive` to assign every Kenso-sendable inbox. Either one fixes the pool to that list. `allInboxes: true` turns on live "All inboxes" instead: every inbox a launch would assign (paused inboxes the campaign lacks and blocked Migma senders left out) joins now and inboxes connected later join too; it ignores the other fields and needs the `sender_routing` flag for the workspace, else `400 validation_failed` with detail code `feature_disabled`. Requires `WriteAccess`. A campaign outside the workspace is `not_found` before the write.
8278
+ Full replace. Send `inboxIds`, or `assignAllActive` to assign every Kenso-sendable inbox. Either one fixes the pool to that list. `allInboxes: true` turns on live "All inboxes" instead: every inbox a launch would assign (paused inboxes the campaign lacks and blocked Migma senders left out) joins now and inboxes connected later join too; it ignores the other fields and needs the `sender_routing` flag for the workspace, else `400 validation_failed` with detail code `feature_disabled`. Requires Campaigns edit. A campaign outside the workspace is `not_found` before the write.
8183
8279
 
8184
8280
  - API: `PUT /v1/campaigns/{campaignId}/inboxes`
8185
8281
  - Generated from the API contract
@@ -8206,7 +8302,7 @@ kenso campaigns replace-inboxes --campaign-id <campaign-id> --data @body.json
8206
8302
 
8207
8303
  Replace the LinkedIn accounts assigned to a campaign.
8208
8304
 
8209
- Full replace. Send `linkedinAccountIds`, or `assignAllConnected` to assign every Connected LinkedIn seat. Requires `WriteAccess`. A campaign outside the workspace is `not_found` before the write.
8305
+ Full replace. Send `linkedinAccountIds`, or `assignAllConnected` to assign every Connected LinkedIn seat. Requires Campaigns edit. A campaign outside the workspace is `not_found` before the write.
8210
8306
 
8211
8307
  - API: `PUT /v1/campaigns/{campaignId}/linkedin-accounts`
8212
8308
  - Generated from the API contract
@@ -8265,7 +8361,7 @@ kenso campaigns replace-linkedin-settings --campaign-id <campaign-id> --data @bo
8265
8361
 
8266
8362
  Turn lead scoring on or off for a campaign.
8267
8363
 
8268
- Requires `WriteAccess`. A campaign outside the workspace is `not_found` before the write.
8364
+ Requires Campaigns edit. A campaign outside the workspace is `not_found` before the write.
8269
8365
 
8270
8366
  - API: `PUT /v1/campaigns/{campaignId}/scoring`
8271
8367
  - Generated from the API contract
@@ -8289,7 +8385,7 @@ kenso campaigns replace-scoring --campaign-id <campaign-id> --data @body.json
8289
8385
 
8290
8386
  Replace a campaign's sender routing rules.
8291
8387
 
8292
- DEV-5225. Full replace: `rules` is every rule, `[]` clears them. Each rule names an active mailbox provider or security gateway `provider` slug (one rule per provider) and at least one whole sending domain in `domains` (lowercased when saved; inboxes added on it later join) or single `inboxIds`. Every domain must be the address domain of a workspace inbox and every inbox a workspace inbox, else `400 validation_failed` with detail codes `unknown_provider`, `unsupported_provider`, `duplicate_provider`, `empty_rule`, `unknown_domain` or `unknown_inbox`. Applies to first emails planned from now on and to queued first emails at send time; follow-ups stay on the first email's inbox. Returns the routing as `GET` does. Needs the `sender_routing` flag (`feature_disabled`). Requires `WriteAccess`. A campaign outside the workspace is `not_found` before the write.
8388
+ DEV-5225. Full replace: `rules` is every rule, `[]` clears them. Each rule names an active mailbox provider or security gateway `provider` slug (one rule per provider) and at least one whole sending domain in `domains` (lowercased when saved; inboxes added on it later join) or single `inboxIds`. Every domain must be the address domain of a workspace inbox and every inbox a workspace inbox, else `400 validation_failed` with detail codes `unknown_provider`, `unsupported_provider`, `duplicate_provider`, `empty_rule`, `unknown_domain` or `unknown_inbox`. Applies to first emails planned from now on and to queued first emails at send time; follow-ups stay on the first email's inbox. Returns the routing as `GET` does. Needs the `sender_routing` flag (`feature_disabled`). Requires Campaigns edit. A campaign outside the workspace is `not_found` before the write.
8293
8389
 
8294
8390
  - API: `PUT /v1/campaigns/{campaignId}/sender-routing`
8295
8391
  - Generated from the API contract
@@ -8311,7 +8407,7 @@ kenso campaigns replace-sender-routing --campaign-id <campaign-id> --data @body.
8311
8407
 
8312
8408
  Restart a campaign's processing.
8313
8409
 
8314
- Re-runs the processing pipeline from the beginning, to recover a campaign that stalled or errored, or to reprocess after a configuration change. On a draft campaign it starts processing behind the same readiness checks as `resume`, and a draft trigger campaign is `400 validation_failed` (start it with `resume`). An archived campaign is `400 validation_failed`. A campaign with `autoLaunchWhenReady` launches once processing reaches ready. Returns the campaign. Requires `WriteAccess` and the launch permission. While the workspace's data is scheduled for deletion or being deleted, returns `409 invalid_state`.
8410
+ Re-runs the processing pipeline from the beginning, to recover a campaign that stalled or errored, or to reprocess after a configuration change. On a draft campaign it starts processing behind the same readiness checks as `resume`, and a draft trigger campaign is `400 validation_failed` (start it with `resume`). An archived campaign is `400 validation_failed`. A campaign with `autoLaunchWhenReady` launches once processing reaches ready. Returns the campaign. Requires Campaigns edit and the `launch` permission. While the workspace's data is scheduled for deletion or being deleted, returns `409 invalid_state`.
8315
8411
 
8316
8412
  - API: `POST /v1/campaigns/{campaignId}/restart`
8317
8413
  - Generated from the API contract
@@ -8330,7 +8426,7 @@ kenso campaigns restart --campaign-id <campaign-id>
8330
8426
 
8331
8427
  Resume sending, processing, and AI workflows together.
8332
8428
 
8333
- Idempotent. A campaign that is already running returns `200` with the resource. A subsystem that was not paused is left as it is. If any subsystem fails, the action returns that error and the caller can retry. A campaign that is still launching returns `409 invalid_state`, and so does every campaign while the workspace's data is scheduled for deletion or being deleted. Requires `WriteAccess`.
8429
+ Idempotent. A campaign that is already running returns `200` with the resource. A subsystem that was not paused is left as it is. If any subsystem fails, the action returns that error and the caller can retry. A campaign that is still launching returns `409 invalid_state`, and so does every campaign while the workspace's data is scheduled for deletion or being deleted. Requires Campaigns edit and the `launch` permission.
8334
8430
 
8335
8431
  - API: `POST /v1/campaigns/{campaignId}/resume`
8336
8432
  - Generated from the API contract
@@ -8349,7 +8445,7 @@ kenso campaigns resume --campaign-id <campaign-id>
8349
8445
 
8350
8446
  Re-queue failed enrichment without restarting the campaign.
8351
8447
 
8352
- Deletes failed website and email-finder markers so existing workers pick the contacts up again. Does not restart processing, and does not change qualification, personalization, or sent state. If data or email enrichment is paused, the markers are still cleared and workers wait until resume. Idempotent: clearing nothing is still `200`. An archived campaign is `validation_failed` and is not touched. A campaign outside the workspace is `not_found` before the retry. Requires `WriteAccess`.
8448
+ Deletes failed website and email-finder markers so existing workers pick the contacts up again. Does not restart processing, and does not change qualification, personalization, or sent state. If data or email enrichment is paused, the markers are still cleared and workers wait until resume. Idempotent: clearing nothing is still `200`. An archived campaign is `validation_failed` and is not touched. A campaign outside the workspace is `not_found` before the retry. Requires Campaigns edit.
8353
8449
 
8354
8450
  - API: `POST /v1/campaigns/{campaignId}/retry-enrichment`
8355
8451
  - Generated from the API contract
@@ -8368,7 +8464,7 @@ kenso campaigns retry-enrichment --campaign-id <campaign-id>
8368
8464
 
8369
8465
  Score contacts in a campaign.
8370
8466
 
8371
- The run finishes in the request and returns a `lead_scoring` job. Poll it with `GET /v1/jobs/{jobId}?type=lead_scoring`. Scores are `qualityScore` on each contact. Requires `WriteAccess`. A campaign outside the workspace is `not_found` before the run.
8467
+ The run finishes in the request and returns a `lead_scoring` job. Poll it with `GET /v1/jobs/{jobId}?type=lead_scoring`. Scores are `qualityScore` on each contact. Requires Campaigns edit and the `ask` permission. A campaign outside the workspace is `not_found` before the run.
8372
8468
 
8373
8469
  - API: `POST /v1/campaigns/{campaignId}/scoring/run`
8374
8470
  - Generated from the API contract
@@ -8391,7 +8487,7 @@ kenso campaigns run-scoring --campaign-id <campaign-id> --data @body.json
8391
8487
 
8392
8488
  Update a campaign's name, contact limit, send schedule, ramp-up, owner, sending switches, same-company rules, account-play protection, new-leads-first order, catch-all lane, language settings, mode, or auto-launch.
8393
8489
 
8394
- Absent fields stay unchanged. `status` is rejected. Unknown JSON members are a 400. A schedule or daily-cap change writes a planner request row (nothing consumes it any more, DEV-1821; gap-fill picks up the change on its next pass); `rampUpDays`, `stopCompanyOnReply` and `groupSameCompanySends` do not, as in the legacy update. `rampUpDays` is 0 (no ramp) to 30 scheduled sending days, else `400 validation_failed`. `stopCompanyOnReply` is the same stored value as `stopSequenceForCompanyOnReply` in the LinkedIn settings; it is written after the other fields. `ownerMemberId` must be an active member of the workspace, else `400 validation_failed` with detail code `not_a_member`. Changing `stopOnReply`, `trackClicks`, `trackOpens`, or `unsubscribeLink` needs the `campaign_send_settings` flag for the workspace, else `400 validation_failed` with detail code `feature_disabled` per field and nothing is written. Sending the stored value is accepted without the flag. `protectAccountPlays` turns skipping people in active account plays on or off for this campaign; people already stopped stay stopped. Requires `WriteAccess`. `newLeadsFirst` true gives first emails the campaign's daily capacity ahead of its follow-ups; it takes effect on the next gap-fill pass. While it is true the response carries `awaitingFirstEmailCount`. `catchAllLaneEnabled` true opts the campaign in to the catch-all lane: the sequencer may also send to unverified CatchAll addresses whose catch-all score clears a bar, capped per campaign and per inbox per day, until the lane's bounce circuit breaker pauses it. It does nothing until Kenso turns the lane on platform-wide. `catchAllLanePausedAt` is system-owned: sending it is an unknown member (`400`). `bookingMeetingTypeId` names a live, enabled round robin meeting type of this workspace that takes this campaign's bookings when the account owner and the sender can't; `null` clears it. `kind` cannot be changed: any value is `400 validation_failed` with detail code `kind_immutable`. `roleId` names a live role of this workspace for a recruiting campaign and needs the `recruiting` flag; `null` clears it. On a sales campaign it is `400` with detail code `not_recruiting`. `excludedCountries` replaces the countries this campaign never sends to: ISO 3166-1 alpha-2 codes or country names, stored as upper-case codes; `[]` clears it. An entry that names no country is `400 validation_failed` with detail code `invalid_country` and nothing is written. Imports drop a contact whose country is listed and the sequencer skips one already on the campaign, from its next pass. The language settings (`sourceLanguage`, `languageMode``source_only` or `auto`, `enabledLanguages`, `formalityOverrides` locale to `formal` or `informal`, and `languageFallback``source` or `hold`) need the `multilingual_outreach` flag for the workspace, else `400 validation_failed` with detail code `feature_disabled` per field sent, even for the stored value. A locale must be a market pack that is not a draft and `sourceLanguage` may also be a language with a primary pack (`en`), else detail code `unknown_language` or `draft_language`; any other bad value is `invalid_value`. `null`, `[]` or `{}` clears `enabledLanguages` or `formalityOverrides`. `mode` is `batch` or `trigger`, else `400 validation_failed`. Batch to trigger converts the campaign to signal-based sending; trigger to batch is `400 validation_failed` with detail code `mode_one_way` and nothing is written. `autoLaunchWhenReady` true launches the campaign once it is ready, with no further confirmation; changing it needs the launch permission (`403 forbidden`), is `400` once the campaign launched or for a trigger or approval-test campaign, and `409 conflict` once auto-launch has run. `mode``trigger` on a batch campaign is `400 validation_failed` on `autoLaunchWhenReady`, and nothing is written, when it comes with `autoLaunchWhenReady` true, or without it while the campaign is set to auto-launch: send `false` in the same request to turn that off.
8490
+ Absent fields stay unchanged. `status` is rejected. Unknown JSON members are a 400. A schedule or daily-cap change writes a planner request row (nothing consumes it any more, DEV-1821; gap-fill picks up the change on its next pass); `rampUpDays`, `stopCompanyOnReply` and `groupSameCompanySends` do not, as in the legacy update. `rampUpDays` is 0 (no ramp) to 30 scheduled sending days, else `400 validation_failed`. `stopCompanyOnReply` is the same stored value as `stopSequenceForCompanyOnReply` in the LinkedIn settings; it is written after the other fields. `ownerMemberId` must be an active member of the workspace, else `400 validation_failed` with detail code `not_a_member`. Changing `stopOnReply`, `trackClicks`, `trackOpens`, or `unsubscribeLink` needs the `campaign_send_settings` flag for the workspace, else `400 validation_failed` with detail code `feature_disabled` per field and nothing is written. Sending the stored value is accepted without the flag. `protectAccountPlays` turns skipping people in active account plays on or off for this campaign; people already stopped stay stopped. Requires Campaigns edit. `newLeadsFirst` true gives first emails the campaign's daily capacity ahead of its follow-ups; it takes effect on the next gap-fill pass. While it is true the response carries `awaitingFirstEmailCount`. `catchAllLaneEnabled` true opts the campaign in to the catch-all lane: the sequencer may also send to unverified CatchAll addresses whose catch-all score clears a bar, capped per campaign and per inbox per day, until the lane's bounce circuit breaker pauses it. It does nothing until Kenso turns the lane on platform-wide. `catchAllLanePausedAt` is system-owned: sending it is an unknown member (`400`). `bookingMeetingTypeId` names a live, enabled round robin meeting type of this workspace that takes this campaign's bookings when the account owner and the sender can't; `null` clears it. `kind` cannot be changed: any value is `400 validation_failed` with detail code `kind_immutable`. `roleId` names a live role of this workspace for a recruiting campaign and needs the `recruiting` flag; `null` clears it. On a sales campaign it is `400` with detail code `not_recruiting`. `excludedCountries` replaces the countries this campaign never sends to: ISO 3166-1 alpha-2 codes or country names, stored as upper-case codes; `[]` clears it. An entry that names no country is `400 validation_failed` with detail code `invalid_country` and nothing is written. Imports drop a contact whose country is listed and the sequencer skips one already on the campaign, from its next pass. The language settings (`sourceLanguage`, `languageMode``source_only` or `auto`, `enabledLanguages`, `formalityOverrides` locale to `formal` or `informal`, and `languageFallback``source` or `hold`) need the `multilingual_outreach` flag for the workspace, else `400 validation_failed` with detail code `feature_disabled` per field sent, even for the stored value. A locale must be a market pack that is not a draft and `sourceLanguage` may also be a language with a primary pack (`en`), else detail code `unknown_language` or `draft_language`; any other bad value is `invalid_value`. `null`, `[]` or `{}` clears `enabledLanguages` or `formalityOverrides`. `mode` is `batch` or `trigger`, else `400 validation_failed`. Batch to trigger converts the campaign to signal-based sending; trigger to batch is `400 validation_failed` with detail code `mode_one_way` and nothing is written. `autoLaunchWhenReady` true launches the campaign once it is ready, with no further confirmation; changing it needs the launch permission (`403 forbidden`), is `400` once the campaign launched or for a trigger or approval-test campaign, and `409 conflict` once auto-launch has run. `mode``trigger` on a batch campaign is `400 validation_failed` on `autoLaunchWhenReady`, and nothing is written, when it comes with `autoLaunchWhenReady` true, or without it while the campaign is set to auto-launch: send `false` in the same request to turn that off.
8395
8491
 
8396
8492
  - API: `PATCH /v1/campaigns/{campaignId}`
8397
8493
  - Generated from the API contract
@@ -8485,7 +8581,7 @@ kenso checkout-sessions create --data @body.json
8485
8581
 
8486
8582
  List stage checkpoints for a build session.
8487
8583
 
8488
- Ordered by id. Read-only. Requires `ReadAccess`.
8584
+ Ordered by id. Read-only. Requires Campaigns view.
8489
8585
 
8490
8586
  - API: `GET /v1/build-sessions/{buildSessionId}/checkpoints` (a list: `--limit`, `--cursor`, `--all`)
8491
8587
  - Generated from the API contract
@@ -8646,7 +8742,7 @@ kenso cli-logins token --data @body.json
8646
8742
 
8647
8743
  Download a weekly click report CSV.
8648
8744
 
8649
- Streams the clicked-leads CSV of the weekly click-report email whose link carries `reportToken`. Requires `ExportAccess` (Export and Campaigns view) in the workspace and account scope all, because the CSV covers every account. An unknown token, or a token of another workspace, is `404 not_found`; an expired report is `410 gone`. The CSV holds lead PII: `Cache-Control: private, no-store`.
8745
+ Streams the clicked-leads CSV of the weekly click-report email whose link carries `reportToken`. Requires Campaigns view and the `export` permission in the workspace and account scope all, because the CSV covers every account. An unknown token, or a token of another workspace, is `404 not_found`; an expired report is `410 gone`. The CSV holds lead PII: `Cache-Control: private, no-store`.
8650
8746
 
8651
8747
  - API: `GET /v1/click-reports/{reportToken}/content`
8652
8748
  - Generated from the API contract
@@ -8984,7 +9080,7 @@ kenso committee-seats delete --seat-id <seat-id>
8984
9080
 
8985
9081
  Return one company in the current workspace.
8986
9082
 
8987
- A missing or cross-workspace id is `not_found`. Requires `ReadAccess`.
9083
+ A missing or cross-workspace id is `not_found`. Requires Campaigns view.
8988
9084
 
8989
9085
  - API: `GET /v1/companies/{companyId}`
8990
9086
  - Generated from the API contract
@@ -9003,7 +9099,7 @@ kenso companies get --company-id <company-id>
9003
9099
 
9004
9100
  List companies in the current workspace.
9005
9101
 
9006
- Ordered by id. `q` matches the name. `domain` is an exact match. Requires `ReadAccess`.
9102
+ Ordered by id. `q` matches the name. `domain` is an exact match. Requires Campaigns view.
9007
9103
 
9008
9104
  - API: `GET /v1/companies` (a list: `--limit`, `--cursor`, `--all`)
9009
9105
  - Generated from the API contract
@@ -9051,7 +9147,7 @@ kenso company-data get
9051
9147
 
9052
9148
  Return the behavior signals of the person behind a contact.
9053
9149
 
9054
- `workspace` holds this workspace's own evidence: `tags` (such as `cold_responder`, `prefers_linkedin`, `best_window:weekday_morning`, `away_until:2026-11-18`), `signals` (`{key, value, rate, touches, responses}`, rate 0 to 1), `touches` and `computedAt` (rebuilt nightly). `network` has the same fields from every Kenso workspace, anonymous, plus `workspaces` (how many other workspaces touched the person); it never says which workspace, sender, campaign or reply, and it is present only with the `behavior_signals_network` feature and when at least one other workspace touched the person. `email_confirmed`, `email_dead_end` and `left_company` are tagged only for the contact's own email address or its company domain. Absent means null: no `workspace` when the workspace has no profile for the person, and an empty object when the contact has no LinkedIn URL and no email. A contact outside the workspace or deleted is `not_found`. Requires `ReadAccess` and the `behavior_signals` feature.
9150
+ `workspace` holds this workspace's own evidence: `tags` (such as `cold_responder`, `prefers_linkedin`, `best_window:weekday_morning`, `away_until:2026-11-18`), `signals` (`{key, value, rate, touches, responses}`, rate 0 to 1), `touches` and `computedAt` (rebuilt nightly). `network` has the same fields from every Kenso workspace, anonymous, plus `workspaces` (how many other workspaces touched the person); it never says which workspace, sender, campaign or reply, and it is present only with the `behavior_signals_network` feature and when at least one other workspace touched the person. `email_confirmed`, `email_dead_end` and `left_company` are tagged only for the contact's own email address or its company domain. Absent means null: no `workspace` when the workspace has no profile for the person, and an empty object when the contact has no LinkedIn URL and no email. A contact outside the workspace or deleted is `not_found`. Requires Campaigns view and the `behavior_signals` feature.
9055
9151
 
9056
9152
  - Beta: needs the `behavior_signals` feature flag on the workspace.
9057
9153
  - API: `GET /v1/contacts/{contactId}/behavior`
@@ -9073,7 +9169,7 @@ kenso contact-behavior get --contact-id <contact-id>
9073
9169
 
9074
9170
  Download a contact export file.
9075
9171
 
9076
- Streams CSV, or a ZIP when a campaign export is one file per segment. Each download is sized from a live count and charged to the export row quota before any row is read; a file over the per-request row cap is `429 quota_exceeded`. Requires `ExportAccess`. A missing or expired export is `not_found`.
9172
+ Streams CSV, or a ZIP when a campaign export is one file per segment. Each download is sized from a live count and charged to the export row quota before any row is read; a file over the per-request row cap is `429 quota_exceeded`. Requires Campaigns view and the `export` permission. A missing or expired export is `not_found`.
9077
9173
 
9078
9174
  - API: `GET /v1/contact-exports/{exportId}/content`
9079
9175
  - Generated from the API contract
@@ -9092,7 +9188,7 @@ kenso contact-exports content --export-id <export-id>
9092
9188
 
9093
9189
  Create a contact export and return its job.
9094
9190
 
9095
- `kind` is `contacts` (workspace filters), `campaign`, `clicked`, `advanced`, or `events`. Campaign kinds require `campaignId` in the current workspace. `events` also requires `occurredAfter` and `occurredBefore`. Requires `ExportAccess`. Poll `GET /v1/jobs/{jobId}?type=contact_export`. The job's `downloadUrl` streams the file. Contacts from a Kenso library list are never exported. For kinds `contacts`, `campaign` and `advanced` the job's `excludedLibraryContacts` says how many were left out, including 0. Kind `contacts` also takes `campaignIds` (1 to 50 campaigns of the workspace, not with `campaignId`), `listId` (a contact's import list), `createdAfter` (inclusive) and `createdBefore` (exclusive), `maxRows` (1 to 50000) and `skipRows` (in contact id order). Other kinds refuse them. `readyOnly` still needs one `campaignId`.
9191
+ `kind` is `contacts` (workspace filters), `campaign`, `clicked`, `advanced`, or `events`. Campaign kinds require `campaignId` in the current workspace. `events` also requires `occurredAfter` and `occurredBefore`. Requires Campaigns view and the `export` permission. Poll `GET /v1/jobs/{jobId}?type=contact_export`. The job's `downloadUrl` streams the file. Contacts from a Kenso library list are never exported. For kinds `contacts`, `campaign` and `advanced` the job's `excludedLibraryContacts` says how many were left out, including 0. Kind `contacts` also takes `campaignIds` (1 to 50 campaigns of the workspace, not with `campaignId`), `listId` (a contact's import list), `createdAfter` (inclusive) and `createdBefore` (exclusive), `maxRows` (1 to 50000) and `skipRows` (in contact id order). Other kinds refuse them. `readyOnly` still needs one `campaignId`.
9096
9192
 
9097
9193
  - API: `POST /v1/contact-exports`
9098
9194
  - Generated from the API contract
@@ -9140,7 +9236,7 @@ kenso contact-exports create --data @body.json
9140
9236
 
9141
9237
  Write a contact export to a new Google Sheet.
9142
9238
 
9143
- Builds the same rows `GET /v1/contact-exports/{exportId}/content` streams and writes them to a new spreadsheet, `Kenso export - {campaign name or Contacts} - {yyyy-MM-dd}`, in the caller's own Google Drive through their Kenso Workspace Google connection. Cells are written as text, never as formulas, and a cell that starts like a formula (`=`, `@`, a tab or line break, or `+` or `-` that is not a number or phone number) keeps a leading apostrophe, as in the CSV download. A campaign export that is one file per segment is `409 invalid_state`: export one segment at a time. Google refusing to create or write the sheet in the member's Drive is `409 invalid_state` too. No usable Google connection is `409 reconnect_required`; Google failing or rate-limiting is `503 provider_unavailable`. Counts against the same export row limits as the download. Requires `ExportAccess` and the `google_sheets` feature flag (404 without it).
9239
+ Builds the same rows `GET /v1/contact-exports/{exportId}/content` streams and writes them to a new spreadsheet, `Kenso export - {campaign name or Contacts} - {yyyy-MM-dd}`, in the caller's own Google Drive through their Kenso Workspace Google connection. Cells are written as text, never as formulas, and a cell that starts like a formula (`=`, `@`, a tab or line break, or `+` or `-` that is not a number or phone number) keeps a leading apostrophe, as in the CSV download. A campaign export that is one file per segment is `409 invalid_state`: export one segment at a time. Google refusing to create or write the sheet in the member's Drive is `409 invalid_state` too. No usable Google connection is `409 reconnect_required`; Google failing or rate-limiting is `503 provider_unavailable`. Counts against the same export row limits as the download. Requires Campaigns view, the `export` permission and the `google_sheets` feature flag (404 without it).
9144
9240
 
9145
9241
  - Beta: needs the `google_sheets` feature flag on the workspace.
9146
9242
  - API: `POST /v1/contact-exports/{exportId}/google-sheet`
@@ -9162,7 +9258,7 @@ kenso contact-exports google-sheet --export-id <export-id>
9162
9258
 
9163
9259
  List contact fields for the column picker, CSV export, and CSV import.
9164
9260
 
9165
- The catalog is static. `usages` is `list`, `export`, and/or `import`. Ordered with `qualityReasoning` beside `qualityScore`. `securityGateway` and `mailboxProvider` are listed only when the workspace has security gateways. `customImage` (an import target) is listed only when the workspace has campaign images. `customVideo` (an import target) is listed only when the workspace has campaign videos. Requires `ReadAccess`.
9261
+ The catalog is static. `usages` is `list`, `export`, and/or `import`. Ordered with `qualityReasoning` beside `qualityScore`. `securityGateway` and `mailboxProvider` are listed only when the workspace has security gateways. `customImage` (an import target) is listed only when the workspace has campaign images. `customVideo` (an import target) is listed only when the workspace has campaign videos. Requires Campaigns view.
9166
9262
 
9167
9263
  - API: `GET /v1/contact-fields` (a list: `--limit`, `--cursor`, `--all`)
9168
9264
  - Generated from the API contract
@@ -9186,7 +9282,7 @@ kenso contact-fields list --all
9186
9282
 
9187
9283
  Preview the buyers Ken finds at a list of companies, for a campaign's ICP.
9188
9284
 
9189
- Body: `campaignId` (required; another workspace's campaign is `404`), `companies` (1 to 50, each `{ key?, name?, domain? }` with a name or a domain, else `400 validation_failed` on `companies[i]`) and `perCompany` (1 to 3, default 3). Send a longer list in parts of 50. Example: `{ "campaignId": 812, "companies": [{ "key": "row-2", "name": "Northwind", "domain": "northwind.com" }], "perCompany": 3 }`. A company with a domain is the Kenso Search company that owns it: one Kenso Search also matches by the given name at that domain, else one whose website is the domain's home page. A company with only a page under the domain (`notion.so/...`) does not count, and no name match stands in for it. A company without a domain is found by a name only one company has. Unmatched companies say why (`acme.com is not in Kenso Search`, `could not match notion.so: no company in Kenso Search has it as its own website`, `no domain, name matches 3 companies`). A matched company lists up to `perCompany` people at it in the campaign's ICP (the titles, seniority, functions and exclusions of its live segments), best first, each with a one-line `reason`, and each person once in the whole answer, by Kenso id or LinkedIn URL (Kenso Search can hold one person twice); when the ICP finds too few, Ken fills the company from a looser search and marks those people `weakMatch`. No ICP ranks people by seniority, and `icp` is empty. `signal` is the company's latest hiring, funding or leadership change from the last 90 days. A company Kenso Search does not answer for in time is `failed`; retry it. Each person's `contact` is a `POST /v1/contact-imports``source: search` contact: post the kept ones unchanged to add them (dedupe, blocklist and lead credits apply there). Nothing is stored and nothing is charged; the companies times `perCompany` count against the workspace's `searchBrowseRows` quota (`429 quota_exceeded` past it). Requires `WriteAccess` and the Import feature.
9285
+ Body: `campaignId` (required; another workspace's campaign is `404`), `companies` (1 to 50, each `{ key?, name?, domain? }` with a name or a domain, else `400 validation_failed` on `companies[i]`) and `perCompany` (1 to 3, default 3). Send a longer list in parts of 50. Example: `{ "campaignId": 812, "companies": [{ "key": "row-2", "name": "Northwind", "domain": "northwind.com" }], "perCompany": 3 }`. A company with a domain is the Kenso Search company that owns it: one Kenso Search also matches by the given name at that domain, else one whose website is the domain's home page. A company with only a page under the domain (`notion.so/...`) does not count, and no name match stands in for it. A company without a domain is found by a name only one company has. Unmatched companies say why (`acme.com is not in Kenso Search`, `could not match notion.so: no company in Kenso Search has it as its own website`, `no domain, name matches 3 companies`). A matched company lists up to `perCompany` people at it in the campaign's ICP (the titles, seniority, functions and exclusions of its live segments), best first, each with a one-line `reason`, and each person once in the whole answer, by Kenso id or LinkedIn URL (Kenso Search can hold one person twice); when the ICP finds too few, Ken fills the company from a looser search and marks those people `weakMatch`. No ICP ranks people by seniority, and `icp` is empty. `signal` is the company's latest hiring, funding or leadership change from the last 90 days. A company Kenso Search does not answer for in time is `failed`; retry it. Each person's `contact` is a `POST /v1/contact-imports``source: search` contact: post the kept ones unchanged to add them (dedupe, blocklist and lead credits apply there). Nothing is stored and nothing is charged; the companies times `perCompany` count against the workspace's `searchBrowseRows` quota (`429 quota_exceeded` past it). Requires Campaigns edit and the `import` permission.
9190
9286
 
9191
9287
  - Beta: needs the `csv_buyer_preview` feature flag on the workspace.
9192
9288
  - API: `POST /v1/contact-imports/buyer-previews`
@@ -9210,7 +9306,7 @@ kenso contact-imports buyer-preview --data @body.json
9210
9306
 
9211
9307
  Start a contact import and return its job.
9212
9308
 
9213
- JSON `source` is `campaign` (copy from `sourceCampaignId`), `search` (a `contacts` array, at most 1,000), `blitz` (the same `contacts` array, at most 1,000, for people picked in the Blitz tab of Search; the run's surface is Blitz and it counts no Kenso Search work), `loopster` (`records`, 1 to 1,000 people picked in the Loopster tab of Search, as its search returned them, and that search's `searchId`: when a record has no LinkedIn URL, Kenso reads it from a temporary table in the workspace's Loopster account, then trashes the table; the run's surface is Loopster and it counts no Kenso Search work) or `people` (`personIds`, 1 to 1,000 workspace people; each becomes a contact with their name, email, title, LinkedIn URL and the company of their highest-priority account; needs the `accounts` feature and Accounts view, and with account scope `own` takes only people on the caller's accounts), `event` (`eventId`, optional `roles` and `includeAccounts`: an outbound event's attendees with an email or a LinkedIn URL, at most 5,000; rows whose company is a workspace account are left out unless `includeAccounts` is true, and then with account scope `own` only those on the caller's accounts are added; needs the `accounts` and `event_outbound` features and Accounts view) or `sales_navigator` (`linkedinAccountId`, a seat the caller connected, and `url`, a Sales Navigator lead search or lead list; its people with their name, current title, LinkedIn profile and company. One import reads at most 8 pages (about 200 to 400 people): a list it cannot read to the end imports nothing and is `400 validation_failed` with code `incomplete_read` on `url`. Behind the `sales_navigator` feature, a seat without Sales Navigator is `409 sales_navigator_required`). Where the `linkedin_network` feature is on, `linkedin_connections` (`connectionIds`, 1 to 1,000 ids from `GET /v1/linkedin-connections`; each becomes a contact with first and last name, title, headline, LinkedIn URL, seniority and Kenso Search's company name, domain, LinkedIn URL, industry and size; needs Accounts view; elsewhere it is an unknown source). Multipart `source=csv` uploads a CSV file into `campaignId`; its optional form field `columnMappings` (a JSON array of mappings, as for a Google Sheet) sets the columns, and a mapped column the header row lacks is `400 validation_failed`; omitted, the header row is mapped by the import's aliases. Behind the `csv_import_check` feature (a field sent while it is off is `400 validation_failed` on that field) the CSV create also takes the List check choices of a scan: `fixes` (a JSON array of `missingKey`, `invalidEmail`, `noMx`, `catchAll`, `personalEmail`, `roleAddress`, `companyContacted`, `companyCap`) drops the rows the List check flags for them and counts each under its key in the job's `skipped`; `includeRecentlyContacted=true` keeps the rows the campaign's recently-emailed window would skip, for this import only; `cleanup=true` cleans the name, email, title, company, domain, LinkedIn URL and phone cells as the scan's cleanup notes say. Omitted, they are off. JSON `source``google_sheets` (behind the `google_sheets` flag) reads `spreadsheetId` (an id or a Google Sheets URL) and the optional `sheet` with the caller's own Kenso Workspace Google connection; row 1 is the header and `columnMappings` work as for a CSV. No connection is `409 reconnect_required`; a sheet not picked for Kenso is `409 sheet_not_shared`. JSON `source``airtable` (behind the `airtable` flag) reads every record of `tableId` (an id or name) in `baseId`, or of its `viewId` when set, with the workspace's Airtable token: the table's field names, primary field first, are the header and each record is a row of cell text. A table or view of more than 10,000 records is `400 validation_failed` on `tableId`: import it in parts through views. No token is `409 not_connected`; a token without the read scopes or access to the base is `409 scope_required`. JSON `source``saved_list` (behind the `saved_lists` flag; elsewhere it is an unknown source) imports the members of `savedListId`: a list of this workspace (a private one only for its owner), a list shared with it, or a library list. The optional `memberFilters` (`country`, `seniority`, `title`, `industry`, `headcountBand`, `hasSendableEmail`) narrow the members. A company list with `roles` (1 to 20 titles) imports up to 5 people per company found by Kenso Search, for at most 500 companies; without `roles` each company is one company contact. `findPeople` must be absent or false. Members this list already put in the campaign are skipped, so a retry adds only the rest. A library list's cached Safe emails land Safe with no email charge on a paid plan; a Free workspace gets no emails from a library list. A member copied out of a library list follows the same rules in any list. `columnMappings` is refused: the list sets its own columns. More than 100,000 contacts is `400 validation_failed` on `memberFilters`. Every source applies the import's dedupe, blocklist and contact limits. A JSON body over 10 MB is `413`; the multipart CSV upload accepts up to 400 MB. Requires `WriteAccess`. Poll `GET /v1/jobs/{jobId}?type=contact_import`. `linkedin_url` (`url`, optional `linkedinAccountId`, `maxLeads`, `postKinds`) queues a LinkedIn import that needs the `linkedin_url_import` flag. Options (DEV-15): `source=campaign` takes `importLimit` (above 0), `importOffset` (0 or more), `onlyImportQualifiedContacts`, `onlyImportValidContacts`, `deduplicateCampaignIds` (1 to 50 campaigns of the workspace; others are `not_found`), `deduplicationMode` (`none`, `by_email` the default, or `by_data`), `allowReimport`, `copyDataEnrichment` and `copyEmailEnrichment` (keep the source's enrichment results; default false). `source=search` (not `blitz`) takes `importLimit`, `triggerPostProcessing` (default true) and `tagIds` (1 to 50 workspace contact tags put on every imported contact). A multipart CSV takes the form fields `importLimit` and `tagIds` (a JSON array, for example `[3,4]`). An option sent with a source that does not honor it, or with `target=people`, is `400 validation_failed` on that field. `source=segment` copies one segment of the workspace (`sourceSegmentId`, from any campaign) into `campaignId` as a new segment with the source segment's name. `copyQualification`, `copyEmailEnrichment` and `copyEmailVerification` keep the source contacts' AI qualification, found emails and email verification (each default false). `segmentId` is refused (the copy makes its own segment); a segment name already in the campaign is `400`, and another import into it in progress is `409`. `target` is `campaign` (the default) or `people`. With `people` (behind the `lead_intake` feature, `404` when off, and Accounts edit) `campaignId` is optional and ignored: the rows of source `csv` (multipart, form field `target=people`), `search`, `blitz`, `event`, `linkedin_connections`, `sales_navigator`, `google_sheets` or `airtable` are queued and run through person intake as workspace people with source `import`. A row needs an email, a LinkedIn URL, or a name with a business company domain; others are skipped. The job's `personImport` reports the created, updated and skipped counts.
9309
+ JSON `source` is `campaign` (copy from `sourceCampaignId`), `search` (a `contacts` array, at most 1,000), `blitz` (the same `contacts` array, at most 1,000, for people picked in the Blitz tab of Search; the run's surface is Blitz and it counts no Kenso Search work), `loopster` (`records`, 1 to 1,000 people picked in the Loopster tab of Search, as its search returned them, and that search's `searchId`: when a record has no LinkedIn URL, Kenso reads it from a temporary table in the workspace's Loopster account, then trashes the table; the run's surface is Loopster and it counts no Kenso Search work) or `people` (`personIds`, 1 to 1,000 workspace people; each becomes a contact with their name, email, title, LinkedIn URL and the company of their highest-priority account; needs the `accounts` feature and Accounts view, and with account scope `own` takes only people on the caller's accounts), `event` (`eventId`, optional `roles` and `includeAccounts`: an outbound event's attendees with an email or a LinkedIn URL, at most 5,000; rows whose company is a workspace account are left out unless `includeAccounts` is true, and then with account scope `own` only those on the caller's accounts are added; needs the `accounts` and `event_outbound` features and Accounts view) or `sales_navigator` (`linkedinAccountId`, a seat the caller connected, and `url`, a Sales Navigator lead search or lead list; its people with their name, current title, LinkedIn profile and company. One import reads at most 8 pages (about 200 to 400 people): a list it cannot read to the end imports nothing and is `400 validation_failed` with code `incomplete_read` on `url`. Behind the `sales_navigator` feature, a seat without Sales Navigator is `409 sales_navigator_required`). Where the `linkedin_network` feature is on, `linkedin_connections` (`connectionIds`, 1 to 1,000 ids from `GET /v1/linkedin-connections`; each becomes a contact with first and last name, title, headline, LinkedIn URL, seniority and Kenso Search's company name, domain, LinkedIn URL, industry and size; needs Accounts view; elsewhere it is an unknown source). Multipart `source=csv` uploads a CSV file into `campaignId`; its optional form field `columnMappings` (a JSON array of mappings, as for a Google Sheet) sets the columns, and a mapped column the header row lacks is `400 validation_failed`; omitted, the header row is mapped by the import's aliases. Behind the `csv_import_check` feature (a field sent while it is off is `400 validation_failed` on that field) the CSV create also takes the List check choices of a scan: `fixes` (a JSON array of `missingKey`, `invalidEmail`, `noMx`, `catchAll`, `personalEmail`, `roleAddress`, `companyContacted`, `companyCap`) drops the rows the List check flags for them and counts each under its key in the job's `skipped`; `includeRecentlyContacted=true` keeps the rows the campaign's recently-emailed window would skip, for this import only; `cleanup=true` cleans the name, email, title, company, domain, LinkedIn URL and phone cells as the scan's cleanup notes say. Omitted, they are off. JSON `source``google_sheets` (behind the `google_sheets` flag) reads `spreadsheetId` (an id or a Google Sheets URL) and the optional `sheet` with the caller's own Kenso Workspace Google connection; row 1 is the header and `columnMappings` work as for a CSV. No connection is `409 reconnect_required`; a sheet not picked for Kenso is `409 sheet_not_shared`. JSON `source``airtable` (behind the `airtable` flag) reads every record of `tableId` (an id or name) in `baseId`, or of its `viewId` when set, with the workspace's Airtable token: the table's field names, primary field first, are the header and each record is a row of cell text. A table or view of more than 10,000 records is `400 validation_failed` on `tableId`: import it in parts through views. No token is `409 not_connected`; a token without the read scopes or access to the base is `409 scope_required`. JSON `source``saved_list` (behind the `saved_lists` flag; elsewhere it is an unknown source) imports the members of `savedListId`: a list of this workspace (a private one only for its owner), a list shared with it, or a library list. The optional `memberFilters` (`country`, `seniority`, `title`, `industry`, `headcountBand`, `hasSendableEmail`) narrow the members. A company list with `roles` (1 to 20 titles) imports up to 5 people per company found by Kenso Search, for at most 500 companies; without `roles` each company is one company contact. `findPeople` must be absent or false. Members this list already put in the campaign are skipped, so a retry adds only the rest. A library list's cached Safe emails land Safe with no email charge on a paid plan; a Free workspace gets no emails from a library list. A member copied out of a library list follows the same rules in any list. `columnMappings` is refused: the list sets its own columns. More than 100,000 contacts is `400 validation_failed` on `memberFilters`. Every source applies the import's dedupe, blocklist and contact limits. A JSON body over 10 MB is `413`; the multipart CSV upload accepts up to 400 MB. Requires Campaigns edit and the `import` permission. Poll `GET /v1/jobs/{jobId}?type=contact_import`. `linkedin_url` (`url`, optional `linkedinAccountId`, `maxLeads`, `postKinds`) queues a LinkedIn import that needs the `linkedin_url_import` flag. Options (DEV-15): `source=campaign` takes `importLimit` (above 0), `importOffset` (0 or more), `onlyImportQualifiedContacts`, `onlyImportValidContacts`, `deduplicateCampaignIds` (1 to 50 campaigns of the workspace; others are `not_found`), `deduplicationMode` (`none`, `by_email` the default, or `by_data`), `allowReimport`, `copyDataEnrichment` and `copyEmailEnrichment` (keep the source's enrichment results; default false). `source=search` (not `blitz`) takes `importLimit`, `triggerPostProcessing` (default true) and `tagIds` (1 to 50 workspace contact tags put on every imported contact). A multipart CSV takes the form fields `importLimit` and `tagIds` (a JSON array, for example `[3,4]`). An option sent with a source that does not honor it, or with `target=people`, is `400 validation_failed` on that field. `source=segment` copies one segment of the workspace (`sourceSegmentId`, from any campaign) into `campaignId` as a new segment with the source segment's name. `copyQualification`, `copyEmailEnrichment` and `copyEmailVerification` keep the source contacts' AI qualification, found emails and email verification (each default false). `segmentId` is refused (the copy makes its own segment); a segment name already in the campaign is `400`, and another import into it in progress is `409`. A campaign or segment copy Kenso cannot start right now (a full import queue, or its job could not be recorded) is `503 service_unavailable`: nothing was copied, so retry it. `target` is `campaign` (the default) or `people`. With `people` (behind the `lead_intake` feature, `404` when off, and Accounts edit) `campaignId` is optional and ignored: the rows of source `csv` (multipart, form field `target=people`), `search`, `blitz`, `event`, `linkedin_connections`, `sales_navigator`, `google_sheets` or `airtable` are queued and run through person intake as workspace people with source `import`. A row needs an email, a LinkedIn URL, or a name with a business company domain; others are skipped. The job's `personImport` reports the created, updated and skipped counts.
9214
9310
 
9215
9311
  - API: `POST /v1/contact-imports`
9216
9312
  - Generated from the API contract
@@ -9389,7 +9485,7 @@ kenso contact-limits update --data @body.json
9389
9485
 
9390
9486
  Soft-delete up to 1,000 contacts by id.
9391
9487
 
9392
- Requires `ReadAccess`, matching `POST /v1/leads/bulk-delete`. Ids outside the workspace are reported in `failed` and are not deleted.
9488
+ Requires Campaigns edit. Ids outside the workspace are reported in `failed` and are not deleted.
9393
9489
 
9394
9490
  - API: `POST /v1/contacts/bulk-delete`
9395
9491
  - Generated from the API contract
@@ -9474,7 +9570,7 @@ kenso contacts bulk-resume --data @body.json
9474
9570
 
9475
9571
  Update fields on up to 1,000 contacts in one call.
9476
9572
 
9477
- Each item is a `contactId` plus any fields `PATCH /v1/contacts/{contactId}` accepts, with the same semantics: absent fields stay unchanged, null clears a string. Items run one at a time in request order. A contact outside the workspace is `not_found` and is not written. A field error fails only that item. `results` is in request order; `status` is `updated`, `not_found`, `validation_failed`, `video_in_use` (the lead's video is used by a sent or queued step), or `failed`. The whole request is rejected, with nothing written, when `items` is empty, has more than 1,000 entries, has an entry that is not an object or lacks a positive `contactId`, or repeats a `contactId`. An item's `customVariables` merges as on the single update. Requires `WriteAccess`, matching the single update.
9573
+ Each item is a `contactId` plus any fields `PATCH /v1/contacts/{contactId}` accepts, with the same semantics: absent fields stay unchanged, null clears a string. Items run one at a time in request order. A contact outside the workspace is `not_found` and is not written. A field error fails only that item. `results` is in request order; `status` is `updated`, `not_found`, `validation_failed`, `video_in_use` (the lead's video is used by a sent or queued step), or `failed`. The whole request is rejected, with nothing written, when `items` is empty, has more than 1,000 entries, has an entry that is not an object or lacks a positive `contactId`, or repeats a `contactId`. An item's `customVariables` merges as on the single update. Requires Campaigns edit.
9478
9574
 
9479
9575
  - API: `POST /v1/contacts/bulk-update`
9480
9576
  - Generated from the API contract
@@ -9495,7 +9591,7 @@ kenso contacts bulk-update --data @body.json
9495
9591
 
9496
9592
  Add up to 1,000 contacts to a campaign.
9497
9593
 
9498
- Uses the existing campaign lead upsert, in batches of 100. Accepted and updated rows are succeeded. A later batch can fail without rolling back earlier rows; the upsert is safe to retry. `customImageUrl` sets the lead's own image (an empty string clears it): a malformed URL fails its row before anything is written, and a refused image (campaign images off for the workspace) fails its row with the contact's id, the contact itself saved. `customVideo` sets the lead's own video (a share link or embed code; an empty string clears it): a value no supported provider matches fails its row before anything is written, a refused video (campaign videos off) fails its row with the contact's id, and a video a sent or queued step uses fails its row with code `video_in_use`, the contact saved and the old video kept. `customVariables` is an object of string values merged into the contact's custom variables (a null or empty value clears that key on an existing contact); a key new to the campaign joins its catalog with AI context off. A blank, unusable or reserved key, or more than 50 keys, fails its row before anything is written; a batch that would take the campaign past 50 custom variables fails every row in it. `emailValidity` (optional, `safe` for an email you already verified) qualifies a new contact on import in a campaign without AI qualification, spending one contact allowance; omitted keeps a stored verdict on an existing email. `useKenEmail` (optional) on a row with no email takes the address from Kenso's email pool when it holds a recently verified one for the person, free, for the first 25 such rows of the request; a row still without an email is refused unless the campaign is LinkedIn-only. Requires `WriteAccess`. `results` has one item per row in request order: `index`, `status` (`created`, `updated`, `skipped` or `failed`), `id` when saved, and for a skipped or failed row a `reason` (`duplicate`, `blocklisted`, `already_contacted`, `limit_reached`, `invalid_email`, `excluded`, `validation` or `other`) and a `message`. A saved row has `sendable`, false when the contact will not send (added unqualified with no contact allowance left, or do-not-contact).
9594
+ Uses the existing campaign lead upsert, in batches of 100. Accepted and updated rows are succeeded. A later batch can fail without rolling back earlier rows; the upsert is safe to retry. `customImageUrl` sets the lead's own image (an empty string clears it): a malformed URL fails its row before anything is written, and a refused image (campaign images off for the workspace) fails its row with the contact's id, the contact itself saved. `customVideo` sets the lead's own video (a share link or embed code; an empty string clears it): a value no supported provider matches fails its row before anything is written, a refused video (campaign videos off) fails its row with the contact's id, and a video a sent or queued step uses fails its row with code `video_in_use`, the contact saved and the old video kept. `customVariables` is an object of string values merged into the contact's custom variables (a null or empty value clears that key on an existing contact); a key new to the campaign joins its catalog with AI context off. A blank, unusable or reserved key, or more than 50 keys, fails its row before anything is written; a batch that would take the campaign past 50 custom variables fails every row in it. `emailValidity` (optional, `safe` for an email you already verified) qualifies a new contact on import in a campaign without AI qualification, spending one contact allowance; omitted keeps a stored verdict on an existing email. `useKenEmail` (optional) on a row with no email takes the address from Kenso's email pool when it holds a recently verified one for the person, free, for the first 25 such rows of the request; a row still without an email is refused unless the campaign is LinkedIn-only. Requires Campaigns edit. `results` has one item per row in request order: `index`, `status` (`created`, `updated`, `skipped` or `failed`), `id` when saved, and for a skipped or failed row a `reason` (`duplicate`, `blocklisted`, `already_contacted`, `limit_reached`, `invalid_email`, `excluded`, `validation` or `other`) and a `message`. A saved row has `sendable`, false when the contact will not send (added unqualified with no contact allowance left, or do-not-contact).
9499
9595
 
9500
9596
  - API: `POST /v1/campaigns/{campaignId}/contacts`
9501
9597
  - Generated from the API contract
@@ -9517,7 +9613,7 @@ kenso contacts create --campaign-id <campaign-id> --data @body.json
9517
9613
 
9518
9614
  Soft-delete one contact.
9519
9615
 
9520
- Requires `WriteAccess`, matching the legacy contact delete.
9616
+ Requires Campaigns edit.
9521
9617
 
9522
9618
  - API: `DELETE /v1/contacts/{contactId}`
9523
9619
  - Generated from the API contract
@@ -9536,7 +9632,7 @@ kenso contacts delete --contact-id <contact-id>
9536
9632
 
9537
9633
  List events for one contact.
9538
9634
 
9539
- Event ids are the stored event id, which is a string. Requires `ReadAccess`. A cursor is valid only with the same `limit` that issued it.
9635
+ Event ids are the stored event id, which is a string. Requires Campaigns view. A cursor is valid only with the same `limit` that issued it.
9540
9636
 
9541
9637
  - API: `GET /v1/contacts/{contactId}/events` (a list: `--limit`, `--cursor`, `--all`)
9542
9638
  - Generated from the API contract
@@ -9583,6 +9679,8 @@ kenso contacts export --campaign 812 --wait
9583
9679
 
9584
9680
  Return one contact in the current workspace.
9585
9681
 
9682
+ Also returns `dataFlags` (flags about the contact's data, as stored) and `personalEmails` (personal addresses a data provider found, newest first, for the reader only: never sent to). The list, search and update responses do not have them.
9683
+
9586
9684
  - API: `GET /v1/contacts/{contactId}`
9587
9685
  - Generated from the API contract
9588
9686
 
@@ -9627,7 +9725,7 @@ kenso contacts import --campaign 812 --file big.csv --server-import --yes
9627
9725
 
9628
9726
  List contacts in the current workspace.
9629
9727
 
9630
- Ordered by id. Requires `ReadAccess`. Each contact on the page counts one row of the workspace's `apiReadRows` quota (DEV-3685); past it, 429 `quota_exceeded` with `Retry-After`.
9728
+ Ordered by id. Requires Campaigns view. Each contact on the page counts one row of the workspace's `apiReadRows` quota (DEV-3685); past it, 429 `quota_exceeded` with `Retry-After`.
9631
9729
 
9632
9730
  - API: `GET /v1/contacts` (a list: `--limit`, `--cursor`, `--all`)
9633
9731
  - Generated from the API contract
@@ -9660,7 +9758,7 @@ kenso contacts list --all
9660
9758
 
9661
9759
  List every scraped website page of the contact's company.
9662
9760
 
9663
- One entry per distinct page with its markdown and scrape date. `source` is `archive` for pages copied from the Common Crawl archive (then `scrapedAt` is the archive capture date), `live` for pages Kenso scraped, and `imported` for content supplied in a CSV upload (then `scrapedAt` is the upload time). Ordered home, about, pricing, products, customer pages, careers, then the rest; newest first within a type. Pages without content are left out. At most 50 pages, not paginated. Requires `ReadAccess`.
9761
+ One entry per distinct page with its markdown and scrape date. `source` is `archive` for pages copied from the Common Crawl archive (then `scrapedAt` is the archive capture date), `live` for pages Kenso scraped, and `imported` for content supplied in a CSV upload (then `scrapedAt` is the upload time). Ordered home, about, pricing, products, customer pages, careers, then the rest; newest first within a type. Pages without content are left out. At most 50 pages, not paginated. Requires Campaigns view.
9664
9762
 
9665
9763
  - API: `GET /v1/contacts/{contactId}/website-pages`
9666
9764
  - Generated from the API contract
@@ -9793,7 +9891,7 @@ kenso contacts resume --where '{"campaignIds": [812], "q": "acme"}' --yes
9793
9891
 
9794
9892
  Search contacts in the current workspace.
9795
9893
 
9796
- Same filters as the list, plus more than one `campaignIds` value, lead `filters` groups, `readyOnly`, and `contactIds`. `q` is the free-text search. Ordered by id unless `sort` is set, in which case the cursor is an offset into that order. No total count. `readyOnly` requires exactly one campaign id. `securityGateway` and `mailboxProvider` filter on the contact's first Safe or Verified email and need security gateways on the workspace (400 otherwise). Requires `ReadAccess`. Pages count on `apiReadRows` like the list.
9894
+ Same filters as the list, plus more than one `campaignIds` value, lead `filters` groups, `readyOnly`, and `contactIds`. `q` is the free-text search. Ordered by id unless `sort` is set, in which case the cursor is an offset into that order. No total count. `readyOnly` requires exactly one campaign id. `securityGateway` and `mailboxProvider` filter on the contact's first Safe or Verified email and need security gateways on the workspace (400 otherwise). Requires Campaigns view. Pages count on `apiReadRows` like the list.
9797
9895
 
9798
9896
  - API: `POST /v1/contacts/search`
9799
9897
  - Generated from the API contract
@@ -10012,7 +10110,7 @@ kenso credit-model-usage create --data @body.json
10012
10110
 
10013
10111
  Return the credit rate card.
10014
10112
 
10015
- The price of every credit action. Static: it does not depend on the workspace and sends no `X-Ken-Workspace-Id`. `millicreditsPerUnit` is per one unit; `unitSize` is the quantity prices are usually quoted for. Domain rows apply per whole dollar of `ceil(registrarUsd x 1.03) + 1`. The `web_scrape` row (per 1,000 pages, at 2x Firecrawl cost) is listed only once Kenso's Firecrawl price is configured, and the `podcast_search` row (per 1,000 Firecrawl credits a podcast lookup uses, at 2x Firecrawl cost) with it. The two places rows (a search page of up to 200 places, and a detail, reviews or photos open past 500 free a day) are listed only while Maps Data can run (MapsData:Enabled with an API key set). Any signed-in person.
10113
+ The price of every credit action. Static: it does not depend on the workspace and sends no `X-Ken-Workspace-Id`. `millicreditsPerUnit` is per one unit; `unitSize` is the quantity prices are usually quoted for. Domain rows apply per whole dollar of `ceil(registrarUsd x 1.03) + 1`. The `web_scrape` row (per 1,000 pages, at 2x Firecrawl cost) is listed only once Kenso's Firecrawl price is configured, and the `podcast_search` row (per 1,000 Firecrawl credits a podcast lookup uses, at 2x Firecrawl cost) with it. The two places rows (a search page of up to 200 places, and a detail, reviews or photos open past 500 free a day) are listed only while Maps Data can run (switched on, with an API key set). Any signed-in person.
10016
10114
 
10017
10115
  - API: `GET /v1/credits/rate-card`
10018
10116
  - Generated from the API contract
@@ -10601,7 +10699,7 @@ kenso crm-sync-issues retry --connection-id <connection-id> --issue-id <issue-id
10601
10699
 
10602
10700
  Declare one custom variable.
10603
10701
 
10604
- Returns `201` and `Location`. An existing key is updated. Requires `WriteAccess`.
10702
+ Returns `201` and `Location`. An existing key is updated. Requires Campaigns edit.
10605
10703
 
10606
10704
  - API: `POST /v1/campaigns/{campaignId}/custom-variables`
10607
10705
  - Generated from the API contract
@@ -10629,7 +10727,7 @@ kenso custom-variables create --campaign-id <campaign-id> --data @body.json
10629
10727
 
10630
10728
  Remove one custom variable.
10631
10729
 
10632
- Requires `WriteAccess`. Returns `204`.
10730
+ Requires Campaigns edit. Returns `204`.
10633
10731
 
10634
10732
  - API: `DELETE /v1/campaigns/{campaignId}/custom-variables/{variableKey}`
10635
10733
  - Generated from the API contract
@@ -10649,7 +10747,7 @@ kenso custom-variables delete --campaign-id <campaign-id> --variable-key <variab
10649
10747
 
10650
10748
  List a campaign's custom variables.
10651
10749
 
10652
- Ordered by key. `q` matches the key or the name. Requires `ReadAccess`.
10750
+ Ordered by key. `q` matches the key or the name. Requires Campaigns view.
10653
10751
 
10654
10752
  - API: `GET /v1/campaigns/{campaignId}/custom-variables` (a list: `--limit`, `--cursor`, `--all`)
10655
10753
  - Generated from the API contract
@@ -10673,7 +10771,7 @@ kenso custom-variables list --campaign-id <campaign-id> --all
10673
10771
 
10674
10772
  Update one custom variable.
10675
10773
 
10676
- Absent fields stay unchanged. Requires `WriteAccess`.
10774
+ Absent fields stay unchanged. Requires Campaigns edit.
10677
10775
 
10678
10776
  - API: `PATCH /v1/campaigns/{campaignId}/custom-variables/{variableKey}`
10679
10777
  - Generated from the API contract
@@ -11097,7 +11195,7 @@ kenso doctor --profile client-a --output json
11097
11195
 
11098
11196
  Start a domain health check.
11099
11197
 
11100
- Returns `202` with a `domain_health` job. A check already running for the workspace is `409 conflict` and `data.id` is that running job. Requires `WriteAccess`. Example: `{ "domainIds": [4, 9] }`.
11198
+ Returns `202` with a `domain_health` job. A check already running for the workspace is `409 conflict` and `data.id` is that running job. Requires Delivery edit. Example: `{ "domainIds": [4, 9] }`.
11101
11199
 
11102
11200
  - API: `POST /v1/domain-health-checks`
11103
11201
  - Generated from the API contract
@@ -11336,7 +11434,7 @@ kenso domain-orders search --data @body.json
11336
11434
 
11337
11435
  Apply one field update to up to 1,000 domains.
11338
11436
 
11339
- Requires `WriteAccess`. Auto-renew requires Delivery edit and the Billing feature. Masking and catch-all require `SuperAdmin`. `websiteMode` (`redirect` or `page`) records the person's answer to "rebuild or redirect"; it cannot be cleared. A `tagIds` entry the workspace has no tag for is `404 not_found`, and nothing is written.
11437
+ Requires Delivery edit. Auto-renew requires Delivery edit and the `billing` permission. `websiteMode` (`redirect` or `page`) records the person's answer to "rebuild or redirect"; it cannot be cleared. A `tagIds` entry the workspace has no tag for is `404 not_found`, and nothing is written.
11340
11438
 
11341
11439
  - API: `POST /v1/domains/bulk-update`
11342
11440
  - Generated from the API contract
@@ -11404,7 +11502,7 @@ kenso domains create-inbox --domain-id <domain-id> --data @body.json
11404
11502
 
11405
11503
  Delete a domain.
11406
11504
 
11407
- Returns `204`. Requires `WriteAccess`.
11505
+ Returns `204`. Requires Delivery edit.
11408
11506
 
11409
11507
  - API: `DELETE /v1/domains/{domainId}`
11410
11508
  - Generated from the API contract
@@ -11496,7 +11594,7 @@ kenso domains list --all
11496
11594
 
11497
11595
  Update a domain. Absent fields stay unchanged. Null clears text fields and tag ids.
11498
11596
 
11499
- Requires `WriteAccess`. Auto-renew requires Delivery edit and the Billing feature. Masking and catch-all require `SuperAdmin`. `websiteMode` (`redirect` or `page`) records the person's answer to "rebuild or redirect"; it cannot be cleared. A `tagIds` entry the workspace has no tag for is `404 not_found`, and nothing is written.
11597
+ Requires Delivery edit. Auto-renew requires Delivery edit and the `billing` permission. `websiteMode` (`redirect` or `page`) records the person's answer to "rebuild or redirect"; it cannot be cleared. A `tagIds` entry the workspace has no tag for is `404 not_found`, and nothing is written.
11500
11598
 
11501
11599
  - API: `PATCH /v1/domains/{domainId}`
11502
11600
  - Generated from the API contract
@@ -11518,7 +11616,7 @@ kenso domains update --domain-id <domain-id> --data @body.json
11518
11616
 
11519
11617
  Verify Kenso tracking DNS for one domain.
11520
11618
 
11521
- Requires `WriteAccess`.
11619
+ Requires Delivery edit.
11522
11620
 
11523
11621
  - API: `POST /v1/domains/{domainId}/verify-dns`
11524
11622
  - Generated from the API contract
@@ -11680,7 +11778,7 @@ kenso email-sequences get --campaign-id <campaign-id>
11680
11778
 
11681
11779
  Preview the campaign email sequence for one contact.
11682
11780
 
11683
- Requires `ReadAccess`. Variable types are lowercase snake_case. Each step shows the text dispatch sends the contact: a contact routed to another language gets the approved localization with its `language`, English `mirrorSubject` and `mirrorBody`, and `qaScore`. `language` previews as a contact in that language instead. Pass `contactId`, or `useRandom=true` to preview a random contact of the campaign; with neither the call is `400 validation_failed` on `contactId`. A contact outside the campaign, or a campaign with no contacts to pick, is `404 not_found`. `senderEmailId` previews from that inbox of the campaign, as the test send sends from it: its names, signature and unsubscribe host fill the steps, and `sender` names it. Without it, or with an inbox the campaign does not use, the first sender by email fills them.
11781
+ Requires Campaigns view. Variable types are lowercase snake_case. Each step shows the text dispatch sends the contact: a contact routed to another language gets the approved localization with its `language`, English `mirrorSubject` and `mirrorBody`, and `qaScore`. `language` previews as a contact in that language instead. Pass `contactId`, or `useRandom=true` to preview a random contact of the campaign; with neither the call is `400 validation_failed` on `contactId`. A contact outside the campaign, or a campaign with no contacts to pick, is `404 not_found`. `senderEmailId` previews from that inbox of the campaign, as the test send sends from it: its names, signature and unsubscribe host fill the steps, and `sender` names it. Without it, or with an inbox the campaign does not use, the first sender by email fills them.
11684
11782
 
11685
11783
  - API: `GET /v1/campaigns/{campaignId}/email-sequence/preview`
11686
11784
  - Generated from the API contract
@@ -11704,7 +11802,7 @@ kenso email-sequences preview --campaign-id <campaign-id>
11704
11802
 
11705
11803
  Replace the campaign email sequence.
11706
11804
 
11707
- Creates the sequence when none exists. Requires `SequenceEditAccess`. A link the URL reputation check refuses is 400 `validation_failed` with one `link_reputation_blocked` detail per link: `field``steps[i].subject` or `steps[i].body`, `rule`, `stepOrder` and, for a saved step, `stepId`. Send `expectedModifiedAt` (the `modifiedAt` from your last read) so an edit made since is not overwritten.
11805
+ Creates the sequence when none exists. Requires Campaigns edit. A link the URL reputation check refuses is 400 `validation_failed` with one `link_reputation_blocked` detail per link: `field``steps[i].subject` or `steps[i].body`, `rule`, `stepOrder` and, for a saved step, `stepId`. Send `expectedModifiedAt` (the `modifiedAt` from your last read) so an edit made since is not overwritten.
11708
11806
 
11709
11807
  - API: `PUT /v1/campaigns/{campaignId}/email-sequence`
11710
11808
  - Generated from the API contract
@@ -11730,7 +11828,7 @@ kenso email-sequences replace --campaign-id <campaign-id> --data @body.json
11730
11828
 
11731
11829
  Send the campaign email sequence to test recipients.
11732
11830
 
11733
- `targetEmails` takes at most 5 valid addresses, none on the workspace blocklist or hard bounced. Once the campaign has inboxes, `senderEmailId` must be one of them. Each recipient x step counts against the workspace's daily test-send emails (429 when used up). Requires `SequenceEditAccess`. Each step goes out as dispatch sends it to the contact, in the contact's language or in `language`; `notes` names a step sent as an approved translation.
11831
+ `targetEmails` takes at most 5 valid addresses, none on the workspace blocklist or hard bounced. Once the campaign has inboxes, `senderEmailId` must be one of them. Each recipient x step counts against the workspace's daily test-send emails (429 when used up). Requires Campaigns edit. Each step goes out as dispatch sends it to the contact, in the contact's language or in `language`; `notes` names a step sent as an approved translation.
11734
11832
 
11735
11833
  - API: `POST /v1/campaigns/{campaignId}/email-sequence/test-send`
11736
11834
  - Generated from the API contract
@@ -11758,7 +11856,7 @@ kenso email-sequences test-send --campaign-id <campaign-id> --data @body.json
11758
11856
 
11759
11857
  Email the conversation to support.
11760
11858
 
11761
- Replaces `POST /v1/support-chat/escalate`. The workspace is `X-Ken-Workspace-Id`. Requires `ReadAccess`.
11859
+ Replaces `POST /v1/support-chat/escalate`. The workspace is `X-Ken-Workspace-Id`. Requires the `ask` permission.
11762
11860
 
11763
11861
  - API: `POST /v1/support/escalations`
11764
11862
  - Generated from the API contract
@@ -13376,7 +13474,7 @@ kenso inbound-webhooks delete --inbound-webhook-id <inbound-webhook-id>
13376
13474
 
13377
13475
  List recent deliveries for an inbound webhook.
13378
13476
 
13379
- The delivery log has no numeric id, so this window uses `limit` (1 to 100, default 100) and omits `totalCount`. Requires `ReadAccess`.
13477
+ The delivery log has no numeric id, so this window uses `limit` (1 to 100, default 100) and omits `totalCount`. Requires the `integrations` permission.
13380
13478
 
13381
13479
  - API: `GET /v1/inbound-webhooks/{inboundWebhookId}/deliveries`
13382
13480
  - Generated from the API contract
@@ -13396,7 +13494,7 @@ kenso inbound-webhooks deliveries --inbound-webhook-id <inbound-webhook-id>
13396
13494
 
13397
13495
  Return one inbound webhook in the current workspace.
13398
13496
 
13399
- A missing or cross-workspace id is `not_found`. Requires `ReadAccess`.
13497
+ A missing or cross-workspace id is `not_found`. Requires the `integrations` permission.
13400
13498
 
13401
13499
  - API: `GET /v1/inbound-webhooks/{inboundWebhookId}`
13402
13500
  - Generated from the API contract
@@ -13415,7 +13513,7 @@ kenso inbound-webhooks get --inbound-webhook-id <inbound-webhook-id>
13415
13513
 
13416
13514
  List a campaign's inbound webhooks in the current workspace.
13417
13515
 
13418
- Ordered by id. Read-only. Requires `ReadAccess`.
13516
+ Ordered by id. Read-only. Requires the `integrations` permission.
13419
13517
 
13420
13518
  - API: `GET /v1/campaigns/{campaignId}/inbound-webhooks` (a list: `--limit`, `--cursor`, `--all`)
13421
13519
  - Generated from the API contract
@@ -13438,7 +13536,7 @@ kenso inbound-webhooks list --campaign-id <campaign-id> --all
13438
13536
 
13439
13537
  List the workspace's people webhooks.
13440
13538
 
13441
- A people webhook runs lead intake on every delivery: it creates or updates a person (source `form` for the form presets, `visitor` for rb2b and vector, else `webhook`). Ordered by id. A campaign's webhooks are listed under the campaign. Requires the `accounts` and `lead_intake` features. Read-only. Requires `ReadAccess`.
13539
+ A people webhook runs lead intake on every delivery: it creates or updates a person (source `form` for the form presets, `visitor` for rb2b and vector, else `webhook`). Ordered by id. A campaign's webhooks are listed under the campaign. Requires the `accounts` and `lead_intake` features. Read-only. Requires the `integrations` permission.
13442
13540
 
13443
13541
  - Beta: needs the `accounts` and `lead_intake` feature flag on the workspace.
13444
13542
  - API: `GET /v1/inbound-webhooks` (a list: `--limit`, `--cursor`, `--all`)
@@ -13626,7 +13724,7 @@ kenso inbox-stock get
13626
13724
 
13627
13725
  Apply one field update to up to 1,000 inboxes.
13628
13726
 
13629
- `paused` pauses or resumes every selected inbox, as on `PATCH /v1/inboxes/{inboxId}`. `profileImageUrl` sets one photo, as on the PATCH, on up to 100 inboxes and takes no other field: `updatedCount` counts the inboxes InboxKit accepted, each other one is in `failed` with the reason. A `tagIds` entry the workspace has no tag for is `404 not_found`, and nothing is written. Needs Delivery edit.
13727
+ `paused` pauses or resumes every selected inbox, as on `PATCH /v1/inboxes/{inboxId}`. `profileImageUrl` sets one photo, as on the PATCH, on up to 100 inboxes and takes no other field: `updatedCount` counts the inboxes InboxKit accepted, each other one is in `failed` with the reason. `forwardingDestination` is set or cleared through InboxKit on each inbox the same way: an inbox InboxKit refused, or one not linked to InboxKit, is in `failed` and left out of `updatedCount`. A destination the request cannot take (malformed, or a Kenso-hosted address), or forwarding being unavailable (InboxKit not configured), is `400`, and nothing is written. A `tagIds` entry the workspace has no tag for is `404 not_found`, and nothing is written. An inbox listed in `failed` is never counted in `updatedCount`, and an id sent twice counts once. Needs Delivery edit.
13630
13728
 
13631
13729
  - API: `POST /v1/inboxes/bulk-update`
13632
13730
  - Generated from the API contract
@@ -13728,7 +13826,7 @@ kenso inboxes create-connect-confirmation --data @body.json
13728
13826
 
13729
13827
  Start a Gmail or Microsoft connect.
13730
13828
 
13731
- `provider` is `google` or `microsoft`. `purpose` is `primary` (the caller's own mailbox, through the Kenso Workspace app; `not_found` without Kenso Accounts) or `campaign` (a shared campaign mailbox, through Kenso Mail). Open `authorizationUrl` before `expiresAt`; nothing is stored until the connect finishes. It finishes in the Kenso dashboard (DEV-2228): after consent the provider sends the browser there, and the member who created the session (for an API key, its owner) must be signed in, in this workspace. Anyone else is refused and nothing is connected, so a key that names no member cannot finish a connect. Requires delivery edit, like the legacy `authorize` routes. A client seat (Delivery view, `approve_own_sends`) connects only its own primary inbox: `purpose=campaign` is `403 forbidden` with `own_inbox_only`, and a login email off the workspace's email domain (or a workspace with none) is `409 invalid_state` with `email_domain_mismatch`, whose message says what fixes it, because that mailbox would never be its primary inbox. A grant for any mailbox but the caller's login email waits for `POST /v1/inboxes/connect-confirmations`, which needs delivery edit, so a client seat connects only its login email. Declared `[WorkspaceMember]`; the action decides delivery edit or `approve_own_sends` (DEV-1811). Optional `returnTo` (DEV-1611) is a dashboard path the finished connect lands on, on the dashboard's own origin, with the usual result query (`gmail=connected`, `microsoft=error&reason=...`); for Microsoft the admin-consent return lands there too. Anything but a relative path with one leading `/`, no `//`, backslash or control characters, at most 512 characters, is `validation_failed` on `returnTo`. Without it the connect returns to the app's default page.
13829
+ `provider` is `google` or `microsoft`. `purpose` is `primary` (the caller's own mailbox, through the Kenso Workspace app; `not_found` without Kenso Accounts) or `campaign` (a shared campaign mailbox, through Kenso Mail). Open `authorizationUrl` before `expiresAt`; nothing is stored until the connect finishes. It finishes in the Kenso dashboard (DEV-2228): after consent the provider sends the browser there, and the member who created the session (for an API key, its owner) must be signed in, in this workspace. Anyone else is refused and nothing is connected, so a key that names no member cannot finish a connect. Requires delivery edit, like the legacy `authorize` routes. A client seat (Delivery view, `approve_own_sends`) connects only its own primary inbox: `purpose=campaign` is `403 forbidden` with `own_inbox_only`, and a login email off the workspace's email domain (or a workspace with none) is `409 invalid_state` with `email_domain_mismatch`, whose message says what fixes it, because that mailbox would never be its primary inbox. A grant for any mailbox but the caller's login email waits for `POST /v1/inboxes/connect-confirmations`, which needs delivery edit, so a client seat connects only its login email. Any member may call it; the action then checks delivery edit or `approve_own_sends` (DEV-1811). Optional `returnTo` (DEV-1611) is a dashboard path the finished connect lands on, on the dashboard's own origin, with the usual result query (`gmail=connected`, `microsoft=error&reason=...`); for Microsoft the admin-consent return lands there too. Anything but a relative path with one leading `/`, no `//`, backslash or control characters, at most 512 characters, is `validation_failed` on `returnTo`. Without it the connect returns to the app's default page.
13732
13830
 
13733
13831
  - API: `POST /v1/inboxes/connect-sessions`
13734
13832
  - Generated from the API contract
@@ -13810,7 +13908,7 @@ kenso inboxes get --inbox-id <inbox-id>
13810
13908
 
13811
13909
  Return warmup state for one inbox.
13812
13910
 
13813
- `status` and `enabled` come from the live warmup provider's own status, picked in the order pause and resume act on. `status` is `pending` while bought warmup waits to activate, and `paused` when it is paused. An Azure inbox with no warmup subscription is `failed`, because Kenso cannot enroll Azure. `readiness` is the cold-send warmup gate's verdict from provider warmup snapshots: `ready`, `bypassed`, `unconfirmed`, `too_young`, `provider_stale`, or `unavailable`. `enrollmentFailure` (staff only) is why the last pre-warmup provider enrollment failed, its consecutive attempts and the next retry time. Needs Delivery view.
13911
+ `status` and `enabled` come from the live warmup provider's own status, picked in the order pause and resume act on. `status` is `pending` while bought warmup waits to activate, and `paused` when it is paused. An Azure inbox with no warmup subscription is `failed`, because Kenso cannot enroll Azure. `readiness` is the cold-send warmup gate's verdict from provider warmup snapshots: `ready`, `bypassed`, `unconfirmed`, `too_young`, `provider_stale`, or `unavailable`. Needs Delivery view.
13814
13912
 
13815
13913
  - API: `GET /v1/inboxes/{inboxId}/warmup`
13816
13914
  - Generated from the API contract
@@ -13879,7 +13977,7 @@ kenso inboxes rename --inbox-id <inbox-id> --data @body.json
13879
13977
 
13880
13978
  Update an inbox. Absent fields stay unchanged. Null clears text fields and forwarding.
13881
13979
 
13882
- `trackOpens` is the owner's Track opens option and exists only on a member's own mailbox (`ownerMemberId` set); on a shared campaign inbox it is `validation_failed`, and anyone but the owner gets `forbidden`. `paused: true` pauses outreach from the inbox and sets `pausedAt` (kept when it is already paused): campaign steps, triggers and approved Accounts 1:1 email wait instead of sending, and new leads are planned on the campaign's other inboxes. `paused: false` resumes it and clears `pausedAt`; held sends go out on the next dispatch. Warmup, reply sync and replies a member sends keep running, and campaign assignments stay. Needs Delivery edit. `profileImageUrl` (https, not null) sets the sender photo: a `POST /v1/inbox-photos` URL as is, any other fetched once (image-from-URL quota), saved only after InboxKit accepts it. Needs the `inbox_photos` flag, else `400` detail code `feature_disabled`, and an inbox Kenso can set (`profilePhotoSupport``google` or `microsoft`), else `400`; either way nothing is written. InboxKit timing out or failing is `503 service_unavailable`: try again. A `tagIds` entry the workspace has no tag for is `404 not_found`, and nothing is written. The update is not atomic: `trackOpens`, then the text fields, then forwarding, then `paused`, then `profileImageUrl` are written in turn, so a failure part-way leaves the earlier ones applied. Each write is idempotent, so retrying the same PATCH is safe.
13980
+ `trackOpens` is the owner's Track opens option and exists only on a member's own mailbox (`ownerMemberId` set); on a shared campaign inbox it is `validation_failed`, and anyone but the owner gets `forbidden`. `paused: true` pauses outreach from the inbox and sets `pausedAt` (kept when it is already paused): campaign steps, triggers and approved Accounts 1:1 email wait instead of sending, and new leads are planned on the campaign's other inboxes. `paused: false` resumes it and clears `pausedAt`; held sends go out on the next dispatch. Warmup, reply sync and replies a member sends keep running, and campaign assignments stay. Needs Delivery edit. `profileImageUrl` (https, not null) sets the sender photo: a `POST /v1/inbox-photos` URL as is, any other fetched once (image-from-URL quota), saved only after InboxKit accepts it. Needs the `inbox_photos` flag, else `400` detail code `feature_disabled`, and an inbox Kenso can set (`profilePhotoSupport``google` or `microsoft`), else `400`; either way nothing is written. InboxKit timing out or failing is `503 service_unavailable`: try again. A `tagIds` entry the workspace has no tag for is `404 not_found`, and nothing is written. A `forwardingDestination` the request cannot take (malformed, or a Kenso-hosted address), forwarding being unavailable (InboxKit not configured), or a `forwardingDestination` (set or null) on an inbox not linked to InboxKit, is `400`, and nothing is written either. The update is not atomic: `trackOpens`, then the text fields, then forwarding, then `paused`, then `profileImageUrl` are written in turn, so a failure part-way leaves the earlier ones applied. Each write is idempotent, so retrying the same PATCH is safe.
13883
13981
 
13884
13982
  - API: `PATCH /v1/inboxes/{inboxId}`
13885
13983
  - Generated from the API contract
@@ -14543,7 +14641,7 @@ kenso jobs cancel --job-id <job-id>
14543
14641
 
14544
14642
  Return one job.
14545
14643
 
14546
- Requires `type` because the same numeric id can exist in more than one source. An `inbox_import` job carries `inboxImport` with counts and per-row results; one with no progress for 10 minutes reads `failed` with `error.code``interrupted`. A missing or cross-workspace id is `not_found`, and so is another member's `knowledge_export`. Read-only. Each type needs what its own routes need, as on the list; without it the call is `403 permission_denied`. A scraping job's status read sets `X-Ken-Web-Source` and `X-Ken-Credits-Charged`: Kenso's key charges a job as its reads report progress. A crawl or batch scrape returns one Firecrawl status page (about 10 MB at most) per read: when `output.next` is not null, pass it back as `cursor` to read the following page. Other types ignore `cursor`. A crawl or batch scrape read draws from the Heavy rate-limit class, like the legacy status routes it replaces.
14644
+ Requires `type` because the same numeric id can exist in more than one source. An `inbox_import` job carries `inboxImport` with counts and per-row results; one with no progress for 10 minutes reads `failed` with `error.code``interrupted`. A missing or cross-workspace id is `not_found`, and so is another member's `knowledge_export`. Read-only. Each type needs what its own routes need, as on the list; without it the call is `403 permission_denied`. A scraping job's status read sets `X-Ken-Web-Source` and `X-Ken-Credits-Charged`: Kenso's key charges a job as its reads report progress. A crawl or batch scrape returns one Firecrawl status page (about 10 MB at most) per read: when `result.next` is not null, pass it back as `cursor` to read the following page. Other types ignore `cursor`. An `extraction` reads `running` while it runs and `failed` once it failed, with the provider's status body in `result`; only an answer that carries no status is `503 service_unavailable`. A crawl or batch scrape read draws from the Heavy rate-limit class, like the legacy status routes it replaces.
14547
14645
 
14548
14646
  - API: `GET /v1/jobs/{jobId}` (a list: `--limit`, `--cursor`, `--all`)
14549
14647
  - Generated from the API contract
@@ -16522,7 +16620,7 @@ kenso learnings list
16522
16620
 
16523
16621
  Start a hosted connect or reconnect session.
16524
16622
 
16525
- Omit the account id to connect a new seat. `linkedInAccountId` or `linkedinAccountId` reconnects that seat. A client seat (Delivery view, `approve_own_sends`) connects only its own LinkedIn: reconnecting a seat another member connected is `403 forbidden` with `not_your_linkedin`, and a new seat while it holds one is `409 conflict` with `reconnect_instead`. Requires Delivery edit, or `approve_own_sends` (declared `[WorkspaceMember]`; the action decides, DEV-1811). `importConnections: true` on a new seat imports its connections into Accounts once it connects, while `linkedin_connections_import` is on (ignored otherwise); a reconnect ignores it and keeps the stored setting (DEV-5778).
16623
+ Omit the account id to connect a new seat. `linkedInAccountId` or `linkedinAccountId` reconnects that seat. A client seat (Delivery view, `approve_own_sends`) connects only its own LinkedIn: reconnecting a seat another member connected is `403 forbidden` with `not_your_linkedin`, and a new seat while it holds one is `409 conflict` with `reconnect_instead`. Requires Delivery edit, or `approve_own_sends` (any member may call it; the action decides, DEV-1811). `importConnections: true` on a new seat imports its connections into Accounts once it connects, while `linkedin_connections_import` is on (ignored otherwise); a reconnect ignores it and keeps the stored setting (DEV-5778).
16526
16624
 
16527
16625
  - API: `POST /v1/linkedin-accounts/connect-sessions`
16528
16626
  - Generated from the API contract
@@ -16684,7 +16782,7 @@ kenso linkedin-accounts update --linkedin-account-id <linkedin-account-id> --dat
16684
16782
 
16685
16783
  List the workspace's LinkedIn connections.
16686
16784
 
16687
- Filters: `linkedinAccountId` (one seat), `icpStatus` (comma = OR: pending, fit, not_fit, unknown), `jobChanged` (`true`: Kenso Search reported a job change within the network's `jobChangeWindowDays`), `seniority` (comma = OR, Kenso Search levels such as entry, senior, manager, director, vp, c_level, owner, partner), `matchStatus` (comma = OR: pending, matched, not_found, failed), `importStatus` (comma = OR: pending, imported, skipped, dismissed; DEV-5778), `companyDomain` (any domain, URL or email; normalized), `q` (name, title or company name contains; at most 300 characters). `sort`: `-createdAt` (default), `-icpScore` (highest first, unscored last), `-connectedAt` (newest first). Each item carries the person (`fullName`, `headline`, `linkedinUrl`, `connectedAt`), Kenso Search's match (`title`, `seniority`, company fields, `jobChangedAt`, `isJobChanged`, previous company and title), the ICP score (`icpStatus`, `icpScore` 0-100, fit at 70, up to 3 `icpReasons`) and `accountId` when a live account the caller can see owns the company domain, and the connections import (`importStatus`, `personId`, `importedAt`, `importSkipCode`, `importAccountId`). `limit` 1-200 (default 50); a cursor is valid only with the same filters and sort. Requires `ReadAccess` and Accounts view.
16785
+ Filters: `linkedinAccountId` (one seat), `icpStatus` (comma = OR: pending, fit, not_fit, unknown), `jobChanged` (`true`: Kenso Search reported a job change within the network's `jobChangeWindowDays`), `seniority` (comma = OR, Kenso Search levels such as entry, senior, manager, director, vp, c_level, owner, partner), `matchStatus` (comma = OR: pending, matched, not_found, failed), `importStatus` (comma = OR: pending, imported, skipped, dismissed; DEV-5778), `companyDomain` (any domain, URL or email; normalized), `q` (name, title or company name contains; at most 300 characters). `sort`: `-createdAt` (default), `-icpScore` (highest first, unscored last), `-connectedAt` (newest first). Each item carries the person (`fullName`, `headline`, `linkedinUrl`, `connectedAt`), Kenso Search's match (`title`, `seniority`, company fields, `jobChangedAt`, `isJobChanged`, previous company and title), the ICP score (`icpStatus`, `icpScore` 0-100, fit at 70, up to 3 `icpReasons`) and `accountId` when a live account the caller can see owns the company domain, and the connections import (`importStatus`, `personId`, `importedAt`, `importSkipCode`, `importAccountId`). `limit` 1-200 (default 50); a cursor is valid only with the same filters and sort. Requires Accounts view.
16688
16786
 
16689
16787
  - Beta: needs the one of `linkedin_connections_import`, `linkedin_network` feature flag on the workspace.
16690
16788
  - API: `GET /v1/linkedin-connections` (a list: `--limit`, `--cursor`, `--all`)
@@ -16717,7 +16815,7 @@ kenso linkedin-connections list --all
16717
16815
 
16718
16816
  Promote LinkedIn connections to Accounts.
16719
16817
 
16720
- 1 to 100 `connectionIds`. Each matched connection's current company (Kenso Search's company domain) becomes an account, or joins the live account that already owns that domain; connections at one company share one account, and each person becomes a stakeholder (role unknown, source search). A new account starts at `stage` (researching, warming or engaged; default researching) with the caller as owner and queues research; an existing account keeps its stage. `accounts` lists each account with `accountId`, `name`, `domain`, `created`, `research` and `people` (`connectionId`, `personId`, `stakeholderId`, `result` added or already_stakeholder). Connections that were not placed are in `skipped` with a `code`: not_found, not_matched (no Kenso Search match yet), no_company_domain, generic_email_domain, domain_taken (another member's account owns the domain and the caller works only its own accounts), previously_removed (a person removed them from the account; agents and API keys do not re-add them), failed (retry). When none can be placed the answer is `400 validation_failed` with one detail per connection, or 500 when one failed on Kenso's side. Campaign sequences are not touched. Requires `WriteAccess`, Accounts edit and the `accounts` feature.
16818
+ 1 to 100 `connectionIds`. Each matched connection's current company (Kenso Search's company domain) becomes an account, or joins the live account that already owns that domain; connections at one company share one account, and each person becomes a stakeholder (role unknown, source search). A new account starts at `stage` (researching, warming or engaged; default researching) with the caller as owner and queues research; an existing account keeps its stage. `accounts` lists each account with `accountId`, `name`, `domain`, `created`, `research` and `people` (`connectionId`, `personId`, `stakeholderId`, `result` added or already_stakeholder). Connections that were not placed are in `skipped` with a `code`: not_found, not_matched (no Kenso Search match yet), no_company_domain, generic_email_domain, domain_taken (another member's account owns the domain and the caller works only its own accounts), previously_removed (a person removed them from the account; agents and API keys do not re-add them), failed (retry). When none can be placed the answer is `400 validation_failed` with one detail per connection, or 500 when one failed on Kenso's side. Campaign sequences are not touched. Requires Accounts edit and the `accounts` feature.
16721
16819
 
16722
16820
  - Beta: needs the `accounts` and `linkedin_network` feature flag on the workspace.
16723
16821
  - API: `POST /v1/linkedin-connections/promote`
@@ -16742,7 +16840,7 @@ kenso linkedin-connections promote --data @body.json
16742
16840
 
16743
16841
  Get the inbound connection request settings of the caller's LinkedIn seats.
16744
16842
 
16745
- `seats` lists the live LinkedIn accounts the caller connected, any mode, oldest first; for a workspace admin also the colleagues' seats that are not off, with `isOwnSeat` false (an admin may only switch those off). Each seat has `linkedinAccountId`, `displayName`, `connectedByMemberId`, `seatStatus` (connecting, connected, credentials_error, restricted, disconnected, deleted, connect_failed), its settings (`mode` off, manual or autonomous; `acceptThreshold` 0-100, default 80; `lane` accounts or campaign; `campaignId`; `sendFirstMessage`; `autonomousEnabledByMemberId`, `autonomousEnabledAt` and `autonomousEnabledVia`: app, the client label of the owner's agent such as Claude Code, or api), its scan (`scanStatus` pending, scanning, idle, paused or blocked; `nextScanAt`, `firstScanCompletedAt`, `lastSuccessAt`, `backoffUntil`, `lastError`) and `counts`: one entry per lane (spam, strong, unclear; omitted before triage) and status (pending_triage, awaiting_decision, accept_queued, accepted, ignored, gone, failed) with a nonzero `count`. A seat never configured shows the defaults (mode off). Requires `ReadAccess` and Accounts view.
16843
+ `seats` lists the live LinkedIn accounts the caller connected, any mode, oldest first; for a workspace admin also the colleagues' seats that are not off, with `isOwnSeat` false (an admin may only switch those off). Each seat has `linkedinAccountId`, `displayName`, `connectedByMemberId`, `seatStatus` (connecting, connected, credentials_error, restricted, disconnected, deleted, connect_failed), its settings (`mode` off, manual or autonomous; `acceptThreshold` 0-100, default 80; `lane` accounts or campaign; `campaignId`; `sendFirstMessage`; `autonomousEnabledByMemberId`, `autonomousEnabledAt` and `autonomousEnabledVia`: app, the client label of the owner's agent such as Claude Code, or api), its scan (`scanStatus` pending, scanning, idle, paused or blocked; `nextScanAt`, `firstScanCompletedAt`, `lastSuccessAt`, `backoffUntil`, `lastError`) and `counts`: one entry per lane (spam, strong, unclear; omitted before triage) and status (pending_triage, awaiting_decision, accept_queued, accepted, ignored, gone, failed) with a nonzero `count`. A seat never configured shows the defaults (mode off). Requires Accounts view.
16746
16844
 
16747
16845
  - Beta: needs the `linkedin_inbound` feature flag on the workspace.
16748
16846
  - API: `GET /v1/linkedin-inbound`
@@ -16763,7 +16861,7 @@ kenso linkedin-inbound get
16763
16861
 
16764
16862
  Change one seat's inbound connection request settings.
16765
16863
 
16766
- Body (absent fields stay as they are): `mode` (off, manual or autonomous), `acceptThreshold` (0-100, the score at or above which autonomous mode accepts a request by itself), `lane` (accounts or campaign: where accepted leads go), `campaignId` (null clears it) and `sendFirstMessage` (the delayed AI first message, accounts lane only). Lane campaign needs a scheduled or sending campaign of the workspace that sends from this seat (a campaign with no seat list sends from every seat); otherwise `400 validation_failed` with `seat_not_in_campaign` on `campaignId`. Moving to lane accounts does not check the stored campaign, so a seat whose campaign stopped being live can always leave the campaign lane. Only the member who connected the seat may change it; a workspace admin may send exactly `{ "mode": "off" }` for a colleague's seat. Anything else on a colleague's seat is `404 not_found`, so an API key with no user, which connected no seat, changes none. The owner's agent or API key changes the seat as the owner, autonomous mode included (DEV-5878); an agent turns autonomous mode on only when its person asked it to. Its change counts as the owner's: on an autonomous seat Ken then accepts strong fits and sends their AI first messages with no one approving. Turning autonomous mode on, or changing an autonomous seat's other settings, also needs the approve permission, as deciding one request does: without it the answer is `403 permission_denied`. Turning autonomous on stamps `autonomousEnabledVia` (app, the `X-Ken-Client` label, or api), and every save writes a Security audit line naming the caller and its client (DEV-5147). Kenso calls no LinkedIn API here; the background scan reads a seat once it is on. Answers the seat as `linkedin-inbound.get` lists it. Requires `WriteAccess` and Accounts edit.
16864
+ Body (absent fields stay as they are): `mode` (off, manual or autonomous), `acceptThreshold` (0-100, the score at or above which autonomous mode accepts a request by itself), `lane` (accounts or campaign: where accepted leads go), `campaignId` (null clears it) and `sendFirstMessage` (the delayed AI first message, accounts lane only). Lane campaign needs a scheduled or sending campaign of the workspace that sends from this seat (a campaign with no seat list sends from every seat); otherwise `400 validation_failed` with `seat_not_in_campaign` on `campaignId`. Moving to lane accounts does not check the stored campaign, so a seat whose campaign stopped being live can always leave the campaign lane. Only the member who connected the seat may change it; a workspace admin may send exactly `{ "mode": "off" }` for a colleague's seat. Anything else on a colleague's seat is `404 not_found`, so an API key with no user, which connected no seat, changes none. The owner's agent or API key changes the seat as the owner, autonomous mode included (DEV-5878); an agent turns autonomous mode on only when its person asked it to. Its change counts as the owner's: on an autonomous seat Ken then accepts strong fits and sends their AI first messages with no one approving. Turning autonomous mode on, or changing an autonomous seat's other settings, also needs the approve permission, as deciding one request does: without it the answer is `403 permission_denied`. Turning autonomous on stamps `autonomousEnabledVia` (app, the `X-Ken-Client` label, or api), and every save writes a Security audit line naming the caller and its client (DEV-5147). Kenso calls no LinkedIn API here; the background scan reads a seat once it is on. Answers the seat as `linkedin-inbound.get` lists it. Requires Accounts edit.
16767
16865
 
16768
16866
  - Beta: needs the `linkedin_inbound` feature flag on the workspace.
16769
16867
  - API: `PATCH /v1/linkedin-inbound/seats/{linkedinAccountId}`
@@ -16794,7 +16892,7 @@ kenso linkedin-inbound-seats update --linkedin-account-id <linkedin-account-id>
16794
16892
 
16795
16893
  Withdraw pending invitations on one of the caller's LinkedIn seats.
16796
16894
 
16797
- Body: `linkedinAccountId` and exactly one of `invitationIds` (1-100 ids from `linkedin-invitations.list`) and `olderThanDays` (1-365: every pending invitation sent more than that many days ago, except those a campaign lane on the seat still waits on, which follow their campaign's own withdraw rule). Nothing reaches LinkedIn now: the withdraws are queued, and the background withdraw sends them paced with the seat's other LinkedIn actions, inside its working hours, at most 25 a day per seat. LinkedIn blocks inviting a withdrawn person again for about three weeks. With ids the answer is `queued` (how many this request queued), `alreadyQueued` (ids already queued, withdrawn or gone, left as they were) and `notFound` (ids never listed on this seat). With days it starts a sweep that reads the seat's whole sent list first (the seat's withdraws wait while it runs) and answers `sweepId`, `olderThanDays` and `status`; a seat's running sweep is answered as it is. Both or neither field is `400 validation_failed`. A seat that is not connected is `409 invalid_state` with `seat_unavailable`. Requires `WriteAccess`, Accounts edit and approve.
16895
+ Body: `linkedinAccountId` and exactly one of `invitationIds` (1-100 ids from `linkedin-invitations.list`) and `olderThanDays` (1-365: every pending invitation sent more than that many days ago, except those a campaign lane on the seat still waits on, which follow their campaign's own withdraw rule). Nothing reaches LinkedIn now: the withdraws are queued, and the background withdraw sends them paced with the seat's other LinkedIn actions, inside its working hours, at most 25 a day per seat. LinkedIn blocks inviting a withdrawn person again for about three weeks. With ids the answer is `queued` (how many this request queued), `alreadyQueued` (ids already queued, withdrawn or gone, left as they were) and `notFound` (ids never listed on this seat). With days it starts a sweep that reads the seat's whole sent list first (the seat's withdraws wait while it runs) and answers `sweepId`, `olderThanDays` and `status`; a seat's running sweep is answered as it is. Both or neither field is `400 validation_failed`. A seat that is not connected is `409 invalid_state` with `seat_unavailable`. Requires Accounts edit and the `approve` permission.
16798
16896
 
16799
16897
  - Beta: needs the `linkedin_inbound` feature flag on the workspace.
16800
16898
  - API: `POST /v1/linkedin-invitations/withdrawals`
@@ -16818,7 +16916,7 @@ kenso linkedin-invitation-withdrawals create --data @body.json
16818
16916
 
16819
16917
  List the withdraws asked for on one of the caller's LinkedIn seats.
16820
16918
 
16821
- Latest request first. `linkedinAccountId` is required; `status` filters by withdraw status (comma separated: queued, withdrawn, gone, failed). `data.items` are invitations with their `withdrawal` (`status`, `requestedAt`, `requestedVia`, `requestedByMemberId`, `sweepId`, `withdrawnAt`, `error`). `gone` means the invitation was no longer pending on LinkedIn, most likely accepted. `data.sweeps` are the seat's latest 10 sweeps (`status` running, done or failed, `queued`, `scannedPages`, `error`; a done sweep with `page_cap_reached` stopped before the oldest invitations). `data.pagination` pages the items; `limit` 1-200 (default 50), and a cursor is valid only with the same filters. Read from Kenso, so a disconnected seat still shows its history. Requires `ReadAccess` and Accounts view.
16919
+ Latest request first. `linkedinAccountId` is required; `status` filters by withdraw status (comma separated: queued, withdrawn, gone, failed). `data.items` are invitations with their `withdrawal` (`status`, `requestedAt`, `requestedVia`, `requestedByMemberId`, `sweepId`, `withdrawnAt`, `error`). `gone` means the invitation was no longer pending on LinkedIn, most likely accepted. `data.sweeps` are the seat's latest 10 sweeps (`status` running, done or failed, `queued`, `scannedPages`, `error`; a done sweep with `page_cap_reached` stopped before the oldest invitations). `data.pagination` pages the items; `limit` 1-200 (default 50), and a cursor is valid only with the same filters. Read from Kenso, so a disconnected seat still shows its history. Requires Accounts view.
16822
16920
 
16823
16921
  - Beta: needs the `linkedin_inbound` feature flag on the workspace.
16824
16922
  - API: `GET /v1/linkedin-invitations/withdrawals` (a list: `--limit`, `--cursor`, `--all`)
@@ -16845,7 +16943,7 @@ kenso linkedin-invitation-withdrawals list --all
16845
16943
 
16846
16944
  List the pending invitations one of the caller's LinkedIn seats sent.
16847
16945
 
16848
- Read live from LinkedIn, newest first. `linkedinAccountId` is required. Each item carries `invitationId` (what a withdraw takes), the invitee (`providerId`, `name`, and `publicIdentifier` and `profileUrl` when LinkedIn sends them, which is rare), `sentAt`, the campaign lane on the seat that invited the same person (`campaignId`, `contactId`; omitted for an invitation sent outside Kenso) and `withdrawal` once someone asked to withdraw it. `limit` 1-100 (default 50). A seat that is not connected is `409 invalid_state` with `seat_unavailable`. When LinkedIn limits the seat's reads the answer is `429 rate_limited` with a `Retry-After` header in seconds. Requires `ReadAccess` and Accounts view.
16946
+ Read live from LinkedIn, newest first. `linkedinAccountId` is required. Each item carries `invitationId` (what a withdraw takes), the invitee (`providerId`, `name`, and `publicIdentifier` and `profileUrl` when LinkedIn sends them, which is rare), `sentAt`, the campaign lane on the seat that invited the same person (`campaignId`, `contactId`; omitted for an invitation sent outside Kenso) and `withdrawal` once someone asked to withdraw it. `limit` 1-100 (default 50). A seat that is not connected is `409 invalid_state` with `seat_unavailable`. When LinkedIn limits the seat's reads the answer is `429 rate_limited` with a `Retry-After` header in seconds. Requires Accounts view.
16849
16947
 
16850
16948
  - Beta: needs the `linkedin_inbound` feature flag on the workspace.
16851
16949
  - API: `GET /v1/linkedin-invitations` (a list: `--limit`, `--cursor`, `--all`)
@@ -16871,7 +16969,7 @@ kenso linkedin-invitations list --all
16871
16969
 
16872
16970
  List companies where ICP-fit connections work that are not accounts yet.
16873
16971
 
16874
- One item per company domain with at least one connection scored fit and no live account owning the domain: `companyDomain`, `companyName`, `kenCompanyId`, `industry`, `employeeCount`, `fitConnectionCount`, `connectionCount`, `topScore` and up to 5 fit `connections`, best score first. Ordered by fit connections, then top score. Promote a company with `linkedin-connections.promote`. `limit` 1-200 (default 50) and `cursor` page by offset. Requires `ReadAccess` and Accounts view.
16972
+ One item per company domain with at least one connection scored fit and no live account owning the domain: `companyDomain`, `companyName`, `kenCompanyId`, `industry`, `employeeCount`, `fitConnectionCount`, `connectionCount`, `topScore` and up to 5 fit `connections`, best score first. Ordered by fit connections, then top score. Promote a company with `linkedin-connections.promote`. `limit` 1-200 (default 50) and `cursor` page by offset. Requires Accounts view.
16875
16973
 
16876
16974
  - Beta: needs the `linkedin_network` feature flag on the workspace.
16877
16975
  - API: `GET /v1/linkedin-network/account-suggestions` (a list: `--limit`, `--cursor`, `--all`)
@@ -16894,7 +16992,7 @@ kenso linkedin-network account-suggestions --all
16894
16992
 
16895
16993
  Get the workspace's LinkedIn network: each seat's sync state and the network counts.
16896
16994
 
16897
- `seats` lists every live LinkedIn account of the workspace, any status, oldest first. Each seat has `linkedinAccountId`, `displayName`, `connectedByMemberId`, `seatStatus` (connecting, connected, credentials_error, restricted, disconnected, deleted, connect_failed), `pausedAt`, and its connections sync: `status` (not_started, pending, syncing, synced, paused, blocked), `connectionCount`, `totalCount`, `initialSyncCompletedAt`, `lastSuccessAt`, `nextRunAt`, `backoffUntil` and `lastError` (rate_limited, timeout, http_403 and similar). Workspace counts over live connections: `connectionCount`, `matchedCount`, `pendingCount` (match or score still pending), `icpFitCount` (score 70 or more), `jobChangedCount` (Kenso Search job change within `jobChangeWindowDays`), `jobChangedIcpFitCount` and `seniorJobChangedIcpFitCount` (also vp, c_level, owner or partner). `hasIcp` is false while the workspace has no ICP text; connections are then scored unknown. Requires `ReadAccess` and Accounts view.
16995
+ `seats` lists every live LinkedIn account of the workspace, any status, oldest first. Each seat has `linkedinAccountId`, `displayName`, `connectedByMemberId`, `seatStatus` (connecting, connected, credentials_error, restricted, disconnected, deleted, connect_failed), `pausedAt`, and its connections sync: `status` (not_started, pending, syncing, synced, paused, blocked), `connectionCount`, `totalCount`, `initialSyncCompletedAt`, `lastSuccessAt`, `nextRunAt`, `backoffUntil` and `lastError` (rate_limited, timeout, http_403 and similar). Workspace counts over live connections: `connectionCount`, `matchedCount`, `pendingCount` (match or score still pending), `icpFitCount` (score 70 or more), `jobChangedCount` (Kenso Search job change within `jobChangeWindowDays`), `jobChangedIcpFitCount` and `seniorJobChangedIcpFitCount` (also vp, c_level, owner or partner). `hasIcp` is false while the workspace has no ICP text; connections are then scored unknown. Requires Accounts view.
16898
16996
 
16899
16997
  - Beta: needs the one of `linkedin_connections_import`, `linkedin_network` feature flag on the workspace.
16900
16998
  - API: `GET /v1/linkedin-network`
@@ -16913,7 +17011,7 @@ kenso linkedin-network get
16913
17011
 
16914
17012
  Ask for a LinkedIn network sync now.
16915
17013
 
16916
- Body (optional): `linkedinAccountId`, one live LinkedIn account of the workspace (404 `not_found` otherwise). Without it, every connected, unpaused seat. Each seat becomes due for the background sync now, never sooner than its `backoffUntil`; a `blocked` seat stays blocked until it is reconnected. Connections whose Kenso Search match failed are queued to match again, and when the workspace has ICP text, matched connections scored unknown are queued to score again. Kenso does not call LinkedIn in this request: the sync runs in the background, so poll `linkedin-network.get`. Answers 202 with the same model as `linkedin-network.get`. Requires `WriteAccess` and Accounts edit.
17014
+ Body (optional): `linkedinAccountId`, one live LinkedIn account of the workspace (404 `not_found` otherwise). Without it, every connected, unpaused seat. Each seat becomes due for the background sync now, never sooner than its `backoffUntil`; a `blocked` seat stays blocked until it is reconnected. Connections whose Kenso Search match failed are queued to match again, and when the workspace has ICP text, matched connections scored unknown are queued to score again. Kenso does not call LinkedIn in this request: the sync runs in the background, so poll `linkedin-network.get`. Answers 202 with the same model as `linkedin-network.get`. Requires Accounts edit.
16917
17015
 
16918
17016
  - Beta: needs the `linkedin_network` feature flag on the workspace.
16919
17017
  - API: `POST /v1/linkedin-network/sync`
@@ -16938,7 +17036,7 @@ kenso linkedin-network sync --data @body.json
16938
17036
 
16939
17037
  Accept or skip a connection request.
16940
17038
 
16941
- Body: `action` (`accept_as_lead`: accept, then hand the person to Accounts or the seat's campaign; `accept_only`: accept and add to the network, nothing else; `skip`: hide it, still pending on LinkedIn) and `updatedAt` from the version the person reviewed (optional; a newer version is `409 conflict` with `concurrent_change`). The same decision as the request's item in Approvals. Only a request on a seat the caller connected can be decided; any other id is `404 not_found`. The seat owner's agent or API key decides as the owner (DEV-5878); an agent decides only when its person asked it to. A key with no user connected no seat, so every id is `404` for it. The decision is recorded as the owner's (`decidedBy` person), with `decidedVia` the `X-Ken-Client` label, or api (DEV-5147): on an autonomous seat its accept's AI first message then goes out with no one approving, as the owner's would; a request with a note still waits for approval. An accept works from awaiting_decision or ignored (a rescued spam item), a skip only from awaiting_decision; anything else is `409 invalid_state` with `invalid_transition`. Accepting on a seat that is not connected is `409 invalid_state` with `seat_unavailable`, and on a seat whose inbound mode is off it is `409 invalid_state` with `seat_off`. An accept queues the request: the paced background accept sends it to LinkedIn later. Unknown actions are `400 validation_failed` with `unknown_action`. Answers the request in its new state. Requires `WriteAccess`, Accounts edit and approve.
17039
+ Body: `action` (`accept_as_lead`: accept, then hand the person to Accounts or the seat's campaign; `accept_only`: accept and add to the network, nothing else; `skip`: hide it, still pending on LinkedIn) and `updatedAt` from the version the person reviewed (optional; a newer version is `409 conflict` with `concurrent_change`). The same decision as the request's item in Approvals. Only a request on a seat the caller connected can be decided; any other id is `404 not_found`. The seat owner's agent or API key decides as the owner (DEV-5878); an agent decides only when its person asked it to. A key with no user connected no seat, so every id is `404` for it. The decision is recorded as the owner's (`decidedBy` person), with `decidedVia` the `X-Ken-Client` label, or api (DEV-5147): on an autonomous seat its accept's AI first message then goes out with no one approving, as the owner's would; a request with a note still waits for approval. An accept works from awaiting_decision or ignored (a rescued spam item), a skip only from awaiting_decision; anything else is `409 invalid_state` with `invalid_transition`. Accepting on a seat that is not connected is `409 invalid_state` with `seat_unavailable`, and on a seat whose inbound mode is off it is `409 invalid_state` with `seat_off`. An accept queues the request: the paced background accept sends it to LinkedIn later. Unknown actions are `400 validation_failed` with `unknown_action`. Answers the request in its new state. Requires Accounts edit and the `approve` permission.
16942
17040
 
16943
17041
  - Beta: needs the `linkedin_inbound` feature flag on the workspace.
16944
17042
  - API: `POST /v1/linkedin-requests/{linkedinRequestId}/decisions`
@@ -16963,7 +17061,7 @@ kenso linkedin-requests decide --linkedin-request-id <linkedin-request-id> --dat
16963
17061
 
16964
17062
  List the connection requests on the caller's LinkedIn seats.
16965
17063
 
16966
- Newest first. Filters: `linkedinAccountId` (one of the caller's seats; a colleague's seat lists nothing), `lane` (spam, strong or unclear) and `status` (pending_triage, awaiting_decision, accept_queued, accepted, ignored, gone or failed); `lane=spam&status=ignored` lists the requests Ken hid as spam, which a person can still accept with `linkedin-requests.decide`. Each item carries the person (`fullName`, `headline`, `note`, `linkedinUrl`, `receivedAt`), Kenso Search's match (`matchStatus`, `title`, `seniority`, company fields), the triage (`lane`, `spamCategory` sellers, recruiters_job_seekers, empty_profile or none; `icpScore` 0-100; up to 3 `icpReasons`), the decision (`decision`, `decidedBy` person or autonomous, `decidedByMemberId`, `decidedVia` app, the client label of the owner's agent or api, `decidedAt`), the accept (`acceptedAt`) and the hand-off (`handoffStatus`, `accountId`, `personId`, `contactId`, `campaignId`). `updatedAt` is the decision version. `limit` 1-200 (default 50); a cursor is valid only with the same filters. Requires `ReadAccess` and Accounts view.
17064
+ Newest first. Filters: `linkedinAccountId` (one of the caller's seats; a colleague's seat lists nothing), `lane` (spam, strong or unclear) and `status` (pending_triage, awaiting_decision, accept_queued, accepted, ignored, gone or failed); `lane=spam&status=ignored` lists the requests Ken hid as spam, which a person can still accept with `linkedin-requests.decide`. Each item carries the person (`fullName`, `headline`, `note`, `linkedinUrl`, `receivedAt`), Kenso Search's match (`matchStatus`, `title`, `seniority`, company fields), the triage (`lane`, `spamCategory` sellers, recruiters_job_seekers, empty_profile or none; `icpScore` 0-100; up to 3 `icpReasons`), the decision (`decision`, `decidedBy` person or autonomous, `decidedByMemberId`, `decidedVia` app, the client label of the owner's agent or api, `decidedAt`), the accept (`acceptedAt`) and the hand-off (`handoffStatus`, `accountId`, `personId`, `contactId`, `campaignId`). `updatedAt` is the decision version. `limit` 1-200 (default 50); a cursor is valid only with the same filters. Requires Accounts view.
16967
17065
 
16968
17066
  - Beta: needs the `linkedin_inbound` feature flag on the workspace.
16969
17067
  - API: `GET /v1/linkedin-requests` (a list: `--limit`, `--cursor`, `--all`)
@@ -17114,7 +17212,7 @@ kenso linkedin-sequences save-version --campaign-id <campaign-id> --data @body.j
17114
17212
 
17115
17213
  Soft-delete a campaign list that no contact references.
17116
17214
 
17117
- A list that still has contacts, including soft-deleted contacts, is `validation_failed`. Requires `WriteAccess`.
17215
+ A list that still has contacts, including soft-deleted contacts, is `validation_failed`. Requires Campaigns edit.
17118
17216
 
17119
17217
  - API: `DELETE /v1/lists/{listId}`
17120
17218
  - Generated from the API contract
@@ -17133,7 +17231,7 @@ kenso lists delete --list-id <list-id>
17133
17231
 
17134
17232
  Return one campaign list in the current workspace.
17135
17233
 
17136
- A missing or cross-workspace id is `not_found`. Requires `ReadAccess`.
17234
+ A missing or cross-workspace id is `not_found`. Requires Campaigns view.
17137
17235
 
17138
17236
  - API: `GET /v1/lists/{listId}`
17139
17237
  - Generated from the API contract
@@ -17152,7 +17250,7 @@ kenso lists get --list-id <list-id>
17152
17250
 
17153
17251
  List a campaign's import lists in the current workspace.
17154
17252
 
17155
- Newest first. `limit` and `cursor` page the set. Read-only. Requires `ReadAccess`.
17253
+ Newest first. `limit` and `cursor` page the set. Read-only. Requires Campaigns view.
17156
17254
 
17157
17255
  - API: `GET /v1/campaigns/{campaignId}/lists` (a list: `--limit`, `--cursor`, `--all`)
17158
17256
  - Generated from the API contract
@@ -17715,7 +17813,7 @@ kenso me delete-avatar
17715
17813
 
17716
17814
  Return the authenticated caller.
17717
17815
 
17718
- Replaces `GET /v1/me/context`, `GET /v1/users/me`, and `GET /v1/workspaces/me`. Read-only. Requires `ReadAccess`. When `X-Ken-Workspace-Id` is present, `role` is the caller's role in that workspace. A super admin's role stays `super_admin`. `features` is always present: the sorted feature flag keys on for the caller, e.g. `["accounts", "app_v4"]`. The workspace is `X-Ken-Workspace-Id` when the caller may act in it, else the caller's only workspace, else none. Gate UI on it; never on flag rows. `permissions` is always present: the caller's effective permissions in that same workspace (template, pages, features, account scope, monthly credit limit, credits used this month), narrowed by an API key's template. It is null for a super admin, when there is no workspace, while the `permissions` feature flag is off for the workspace (the default), or when permissions could not be evaluated (including an API key that no longer resolves). `hasPaidPlan` is always present: whether that same workspace is on a paid plan, null for a super admin, when there is no workspace, or when the plan could not be read. `role` is deprecated.
17816
+ Replaces `GET /v1/me/context`, `GET /v1/users/me`, and `GET /v1/workspaces/me`. Read-only. Open to any signed-in caller. When `X-Ken-Workspace-Id` is present, `role` is the caller's role in that workspace. A Kenso staff caller keeps its own staff role. `features` is always present: the sorted feature flag keys on for the caller, e.g. `["accounts", "studio"]`. The workspace is `X-Ken-Workspace-Id` when the caller may act in it, else the caller's only workspace, else none. Gate UI on it; never on flag rows. `permissions` is always present: the caller's effective permissions in that same workspace (template, pages, features, account scope, monthly credit limit, credits used this month), narrowed by an API key's template. It is null for Kenso staff, when there is no workspace, while the `permissions` feature flag is off for the workspace (the default), or when permissions could not be evaluated (including an API key that no longer resolves). `hasPaidPlan` is always present: whether that same workspace is on a paid plan, null for Kenso staff, when there is no workspace, or when the plan could not be read. `role` is deprecated.
17719
17817
 
17720
17818
  - API: `GET /v1/me`
17721
17819
  - Generated from the API contract
@@ -17771,7 +17869,7 @@ kenso me get-calendar-preferences
17771
17869
 
17772
17870
  Return the caller's notification preferences.
17773
17871
 
17774
- One row per event: `isEnabled` is the Email toggle as before, `channels` holds the In app, Email, Slack and Teams choices (a saved choice, else the event's default) and `surface` is `classic` or `accounts`. Accounts events are listed when the `notifications` feature is on for the workspace or for any other workspace the caller is a member of, since those workspaces email the caller; their In app, Slack and Teams toggles change only where the feature is on. Always-on system events (billing, onboarding, domain import, access requests) are listed only for a super admin, with `isLocked` true; everyone else gets them on their defaults. `dailyBrief` is the brief time and time zone, and `destinations` the caller's email and Slack channel in the workspace. The workspace is `X-Ken-Workspace-Id` when the caller is a member, else the caller's only workspace, else none. Read-only. Requires `ReadAccess`.
17872
+ One row per event: `isEnabled` is the Email toggle as before, `channels` holds the In app, Email, Slack and Teams choices (a saved choice, else the event's default) and `surface` is `classic` or `accounts`. Accounts events are listed when the `notifications` feature is on for the workspace or for any other workspace the caller is a member of, since those workspaces email the caller; their In app, Slack and Teams toggles change only where the feature is on. Always-on system events (billing, onboarding, domain import, access requests) are listed only for a super admin, with `isLocked` true; everyone else gets them on their defaults. `dailyBrief` is the brief time and time zone, and `destinations` the caller's email and Slack channel in the workspace. The workspace is `X-Ken-Workspace-Id` when the caller is a member, else the caller's only workspace, else none. Read-only. Open to any workspace member.
17775
17873
 
17776
17874
  - API: `GET /v1/me/notification-preferences`
17777
17875
  - Generated from the API contract
@@ -17807,7 +17905,7 @@ kenso me get-preferences
17807
17905
 
17808
17906
  Return how the caller signs in.
17809
17907
 
17810
- `emailSwitchAvailable` is true when the app may offer to move the caller's Google, Microsoft or LinkedIn sign-in onto an email login (`POST /v1/me/sign-in/switch`): `Auth0:SignInEmailSwitch:Enabled` is on, the caller is a person signed in to the Kenso app (not an API key or an agent), and the account is not a Kenso team or staff one. `409 conflict` when the account no longer signs in with the caller's sign-in: a switch moved it, and the API has not refused that token yet (up to 30 s). Read-only. Requires `ReadAccess`.
17908
+ `emailSwitchAvailable` is true when the app may offer to move the caller's Google, Microsoft or LinkedIn sign-in onto an email login (`POST /v1/me/sign-in/switch`): `Auth0:SignInEmailSwitch:Enabled` is on, the caller is a person signed in to the Kenso app (not an API key or an agent), and the account is not a Kenso team or staff one. `409 conflict` when the account no longer signs in with the caller's sign-in: a switch moved it, and the API has not refused that token yet (up to 30 s). Read-only. Open to any workspace member.
17811
17909
 
17812
17910
  - API: `GET /v1/me/sign-in`
17813
17911
  - Generated from the API contract
@@ -17843,7 +17941,7 @@ kenso me get-signature
17843
17941
 
17844
17942
  Connect another Google, LinkedIn or Microsoft work login to the caller's sign-in.
17845
17943
 
17846
- The Kenso app calls it after the member signed in to the other login in its link round-trip, with that login's ID token. Only a person signed in to the Kenso app may link: an API key, an MCP client or an agent gets `403 forbidden`. `403 feature_off` unless the `security_actions` feature is on for the workspace. The token must be one Auth0 issued the Kenso app (signature, issuer, audience), at most ten minutes old (`400 login_expired`), else `400 invalid_login`; the provider must have verified its email (`400 unverified_email`), which may differ from the account's; and the login must be Google, LinkedIn or Microsoft work (`400 provider_not_allowed`). Auth0 never marks an Entra email verified, so a Microsoft work login answers `400 unverified_email` unless `Auth0:SignInEmailSwitch:MicrosoftConnectEnabled` is on (DEV-5799): then it links with an unverified email, which is only a label and finds, binds, grants or accepts nothing; the round-trip's fresh sign-in is the proof. `409 already_linked` when it is the caller's own sign-in, or a retry of the caller's own link that Auth0 made (the account below is closed then). `409 login_in_use` when another Kenso account signs in with that login or holds its address, read before any Auth0 change. The one exception is the account Auth0's signup made for this login during the round-trip: made in the last 30 minutes, after the Auth0 login, with no workspace. Auth0 then folds the login into the caller's, so it signs in to this account, and that account is closed (`previousAccount`). `503` when Auth0 cannot be reached or another switch or link runs; when Auth0 did not answer the link, reload the logins before retrying. Records `sign_in.login_linked` in each of the caller's workspaces. Requires `ReadAccess`.
17944
+ The Kenso app calls it after the member signed in to the other login in its link round-trip, with that login's ID token. Only a person signed in to the Kenso app may link: an API key, an MCP client or an agent gets `403 forbidden`. `403 feature_off` unless the `security_actions` feature is on for the workspace. The token must be one Auth0 issued the Kenso app (signature, issuer, audience), at most ten minutes old (`400 login_expired`), else `400 invalid_login`; the provider must have verified its email (`400 unverified_email`), which may differ from the account's; and the login must be Google, LinkedIn or Microsoft work (`400 provider_not_allowed`). Auth0 never marks an Entra email verified, so a Microsoft work login answers `400 unverified_email` unless `Auth0:SignInEmailSwitch:MicrosoftConnectEnabled` is on (DEV-5799): then it links with an unverified email, which is only a label and finds, binds, grants or accepts nothing; the round-trip's fresh sign-in is the proof. `409 already_linked` when it is the caller's own sign-in, or a retry of the caller's own link that Auth0 made (the account below is closed then). `409 login_in_use` when another Kenso account signs in with that login or holds its address, read before any Auth0 change. The one exception is the account Auth0's signup made for this login during the round-trip: made in the last 30 minutes, after the Auth0 login, with no workspace. Auth0 then folds the login into the caller's, so it signs in to this account, and that account is closed (`previousAccount`). `503` when Auth0 cannot be reached or another switch or link runs; when Auth0 did not answer the link, reload the logins before retrying. Records `sign_in.login_linked` in each of the caller's workspaces. Open to any workspace member.
17847
17945
 
17848
17946
  - API: `POST /v1/me/sign-in/link`
17849
17947
  - Generated from the API contract
@@ -17996,7 +18094,7 @@ kenso me register-session --data @body.json
17996
18094
 
17997
18095
  Replace the caller's avatar.
17998
18096
 
17999
- Multipart field `file` must be a non-empty PNG, JPEG, or WebP under 10 MB. Requires `ReadAccess`.
18097
+ Multipart field `file` must be a non-empty PNG, JPEG, or WebP under 10 MB. Open to any workspace member.
18000
18098
 
18001
18099
  - API: `PUT /v1/me/avatar`
18002
18100
  - Generated from the API contract
@@ -18017,7 +18115,7 @@ kenso me replace-avatar --file ./file.csv
18017
18115
 
18018
18116
  Schedule deletion of the caller's account in 14 days.
18019
18117
 
18020
- Idempotent while a deletion is already pending. Refused when the caller is the last admin of a workspace. Requires `ReadAccess`.
18118
+ Idempotent while a deletion is already pending. Refused when the caller is the last admin of a workspace. Open to any workspace member.
18021
18119
 
18022
18120
  - API: `POST /v1/me/deletion-request`
18023
18121
  - Generated from the API contract
@@ -18095,7 +18193,7 @@ kenso me revoke-session --session-id <session-id>
18095
18193
 
18096
18194
  Move the caller's Google, Microsoft or LinkedIn sign-in onto an email login.
18097
18195
 
18098
- The Kenso app calls it once the confirmation link it sent to the new address is opened, with the Auth0 database login it created for that address. Only a person signed in to the Kenso app may switch: an API key, an MCP client or an agent gets `403 forbidden`. `403 feature_off` unless `Auth0:SignInEmailSwitch:Enabled`. The login must be a verified database login for `email`, made for this account and this sign-in, and the address must not be a Kenso team one; it may be the address the old sign-in uses. Its Auth0 organizations are copied first; then the account moves onto it (same user id, workspaces and roles), and the old sign-in is linked into it when Auth0 verified the old sign-in's address, so it still signs in to this account (`linked`), else blocked, then deleted. A failure before the move leaves the account as it was. `409 email_taken` when another Kenso account uses the address, `409 conflict` when the account changed meanwhile, `503` when Auth0 cannot be reached or another switch runs. The caller's token belongs to the retired sign-in: sign the session out afterwards. Requires `ReadAccess`. Cleanup contract (DEV-4247). The new login: the Kenso app never deletes it, and a retry reuses its marked login while Auth0 still has it. Before this endpoint has read the login as made for this account and the caller's sign-in, with no account on it, it deletes nothing: `403 feature_off`, a `400` for a malformed request or a login Auth0 does not have, `403` for a login made for another account or sign-in, `409` for a login an account holds, a `503` when another switch runs or Auth0 cannot be read, and `404`. After that, every refusal deletes it (`409 conflict` when the account left the caller's sign-in included), unless the switch's lease ran short: then the refusal keeps the login, whatever it answers, since another submit may be moving the account onto it, and a lease found short before the move answers `503`. The old sign-in: when Auth0 does not answer the link, or refuses the block or the delete, it stays marked retired (until Auth0 confirms the link or the block, then `Auth0:SignInEmailSwitch:RetiredFor`), so its tokens reach no account, and `SignInEmailSwitchCleanupJob` on clickevent.processor retries. The `moved_at` stamp (DEV-4247). Once the account is on the new login, and before the old sign-in is linked or retired, the switch writes `app_metadata.ken_sign_in_switch.moved_at` (ISO-8601 UTC) on that login, in one write with its `ken_user_id` and `from_user_id`. It never writes it before the move commits. The Kenso app finishes a switch whose answer it lost only on that stamp. When Auth0 refuses it, the switch still answers `200`, and `SignInEmailSwitchCleanupJob` writes it. A retry for an account already on that login answers `409 conflict` and writes a missing stamp first.
18196
+ The Kenso app calls it once the confirmation link it sent to the new address is opened, with the Auth0 database login it created for that address. Only a person signed in to the Kenso app may switch: an API key, an MCP client or an agent gets `403 forbidden`. `403 feature_off` unless `Auth0:SignInEmailSwitch:Enabled`. The login must be a verified database login for `email`, made for this account and this sign-in, and the address must not be a Kenso team one; it may be the address the old sign-in uses. Its Auth0 organizations are copied first; then the account moves onto it (same user id, workspaces and roles), and the old sign-in is linked into it when Auth0 verified the old sign-in's address, so it still signs in to this account (`linked`), else blocked, then deleted. A failure before the move leaves the account as it was. `409 email_taken` when another Kenso account uses the address, `409 conflict` when the account changed meanwhile, `503` when Auth0 cannot be reached or another switch runs. The caller's token belongs to the retired sign-in: sign the session out afterwards. Open to any workspace member. Cleanup contract (DEV-4247). The new login: the Kenso app never deletes it, and a retry reuses its marked login while Auth0 still has it. Before this endpoint has read the login as made for this account and the caller's sign-in, with no account on it, it deletes nothing: `403 feature_off`, a `400` for a malformed request or a login Auth0 does not have, `403` for a login made for another account or sign-in, `409` for a login an account holds, a `503` when another switch runs or Auth0 cannot be read, and `404`. After that, every refusal deletes it (`409 conflict` when the account left the caller's sign-in included), unless the switch's lease ran short: then the refusal keeps the login, whatever it answers, since another submit may be moving the account onto it, and a lease found short before the move answers `503`. The old sign-in: when Auth0 does not answer the link, or refuses the block or the delete, it stays marked retired (until Auth0 confirms the link or the block, then `Auth0:SignInEmailSwitch:RetiredFor`), so its tokens reach no account, and `SignInEmailSwitchCleanupJob` on clickevent.processor retries. The `moved_at` stamp (DEV-4247). Once the account is on the new login, and before the old sign-in is linked or retired, the switch writes `app_metadata.ken_sign_in_switch.moved_at` (ISO-8601 UTC) on that login, in one write with its `ken_user_id` and `from_user_id`. It never writes it before the move commits. The Kenso app finishes a switch whose answer it lost only on that stamp. When Auth0 refuses it, the switch still answers `200`, and `SignInEmailSwitchCleanupJob` writes it. A retry for an account already on that login answers `409 conflict` and writes a missing stamp first.
18099
18197
 
18100
18198
  - API: `POST /v1/me/sign-in/switch`
18101
18199
  - Generated from the API contract
@@ -18118,7 +18216,7 @@ kenso me switch-sign-in --data @body.json
18118
18216
 
18119
18217
  Update the caller's profile.
18120
18218
 
18121
- `linkedinUrl`, `firstName`, `lastName`, `jobTitle`, `timeZone` and `language`: absent is unchanged, null or blank clears it. Values are trimmed. `firstName` and `lastName` are at most 100 characters, `jobTitle` at most 200, `timeZone` must be an IANA id such as `America/New_York`, and `language` a BCP 47 tag such as `fr` or `fr-FR` (stored cased, so `fr-fr` is `fr-FR`; null means en); otherwise `validation_failed` with one detail per field. The time zone is stored and returned only. A name saved here is kept on later sign-ins. Saving a URL may enqueue a best-effort avatar derivation when no photo exists. Requires `ReadAccess` because every role can edit their own profile.
18219
+ `linkedinUrl`, `firstName`, `lastName`, `jobTitle`, `timeZone` and `language`: absent is unchanged, null or blank clears it. Values are trimmed. `firstName` and `lastName` are at most 100 characters, `jobTitle` at most 200, `timeZone` must be an IANA id such as `America/New_York`, and `language` a BCP 47 tag such as `fr` or `fr-FR` (stored cased, so `fr-fr` is `fr-FR`; null means en); otherwise `validation_failed` with one detail per field. The time zone is stored and returned only. A name saved here is kept on later sign-ins. Saving a URL may enqueue a best-effort avatar derivation when no photo exists. Open to any workspace member, since everyone can edit their own profile.
18122
18220
 
18123
18221
  - API: `PATCH /v1/me`
18124
18222
  - Generated from the API contract
@@ -18235,7 +18333,7 @@ kenso me update-notification --notification-id <notification-id> --data @body.js
18235
18333
 
18236
18334
  Update the caller's notification preferences.
18237
18335
 
18238
- Send only the changed events. Each item sets `isEnabled` (the Email toggle), `channels` (any of `inApp`, `email`, `slack`, `teams`, `push`), or both; `isEnabled` and `channels.email` must agree. Unknown event keys reject the whole request, and so does an `isLocked` event unless the caller is a super admin (detail code `locked`). In app, Slack, Teams and Push on an Accounts event need the `notifications` feature. `dailyBrief.time` is HH:mm and `dailyBrief.timeZone` an IANA time zone. Returns the updated preferences. Requires `ReadAccess`.
18336
+ Send only the changed events. Each item sets `isEnabled` (the Email toggle), `channels` (any of `inApp`, `email`, `slack`, `teams`, `push`), or both; `isEnabled` and `channels.email` must agree. Unknown event keys reject the whole request, and so does an `isLocked` event unless the caller is a super admin (detail code `locked`). In app, Slack, Teams and Push on an Accounts event need the `notifications` feature. `dailyBrief.time` is HH:mm and `dailyBrief.timeZone` an IANA time zone. Returns the updated preferences. Open to any workspace member.
18239
18337
 
18240
18338
  - API: `PATCH /v1/me/notification-preferences`
18241
18339
  - Generated from the API contract
@@ -18952,7 +19050,7 @@ kenso meetings create --data @body.json
18952
19050
 
18953
19051
  Plan the meeting's follow-up now ("Draft follow-up").
18954
19052
 
18955
- No body. For a meeting that got no automatic follow-up: it plans the touch, and Ken's writer drafts it on its next pass as the meeting's owner (poll GET until `status` leaves planned). It skips the held check and the owner's "Draft a follow-up after each call" setting, and keeps every other rule. `201` with the touch when it planned one; `200` with the existing one when the meeting already has it (one per meeting, never redrafted: rewrite it with POST /v1/touches/{touchId}/rewrite). `409 invalid_state` when the meeting gets none, with the reason as `error.details[0].code`: `no_account`, `private`, `no_touch_stage`, `no_notes`, `no_recipient`, `flag_off` (Ken's writer is off for the workspace) or `follow_up_deleted`. Nothing is sent without an approval. Flags `meetings` and `meeting_follow_up`; meetings edit, and never a client seat.
19053
+ No body. For a meeting that got no automatic follow-up: it plans the touch, and Ken's writer drafts it on its next pass as the meeting's owner (poll GET until `status` leaves planned). It skips the held check and the owner's "Draft a follow-up after each call" setting, and keeps every other rule. `201` with the touch when it planned one; `200` with the existing one when the meeting already has it (one per meeting, never redrafted: rewrite it with POST /v1/touches/{touchId}/rewrite). `409 invalid_state` when the meeting gets none, with the reason as `error.details[0].code`: `no_account`, `private`, `no_touch_stage`, `no_notes`, `no_recipient`, `recipient_suppressed` (the guest unsubscribed, bounced or is on the blocklist), `flag_off` (Ken's writer is off for the workspace) or `follow_up_deleted`. When you already have an email thread with the guest, the follow-up replies in it from the inbox it lives in (`inReplyToMessageId` and `senderInboxId` on the touch); a member's own mailbox only when it is the meeting owner's. Nothing is sent without an approval. Flags `meetings` and `meeting_follow_up`; meetings edit, and never a client seat.
18956
19054
 
18957
19055
  - Beta: needs the `meeting_follow_up` and `meetings` feature flag on the workspace.
18958
19056
  - API: `POST /v1/meetings/{meetingId}/follow-up`
@@ -19238,6 +19336,29 @@ List the meeting's notes sources, the facts they produced and the new people on
19238
19336
  kenso meetings list-sources --meeting-id <meeting-id>
19239
19337
  ```
19240
19338
 
19339
+ ### kenso meetings next-step-tasks
19340
+
19341
+ Add a meeting's next steps as tasks.
19342
+
19343
+ Body `{ "lines": [ { "text": "Send the pilot pricing to Maya", "dueAt": "2026-11-05T09:00:00Z" } ] }`: 1 to 20 lines, each `text` trimmed with whitespace runs made one space, then 1 to 500 characters, `dueAt` optional (without it the task is due now). Each line becomes one touch on the meeting's account: `channel` task, `reason` manual, `meetingId` the meeting, `title` the line (a line over 200 characters is cut at a word with "…" for the title and kept whole as the `body`), created by the caller. Returns `{ "items": [...], "created": 1 }`, each item the touch as GET /v1/touches/{touchId} returns it, in line order, and `created`, the number of tasks this request wrote (always present, `0` included). Safe to retry: a line that already has its task (the same text, ignoring case and spacing) returns it and writes nothing, so a repeated line is one task. A line whose task was deleted stays deleted: it is left out of `items` and listed in `skipped` as `{ "text", "reason": "deleted" }` (`not_visible` for a task on an account you cannot see). `201` when it created a task, `200` when every line already had one. `400 validation_failed` for no lines, more than 20, or a blank or too long line (nothing written); `404` for a meeting you cannot see; `409 invalid_state` with `error.details[0].code``no_account` when the meeting has no account (or one outside your account scope). Nothing is sent. Flags `meetings` and `accounts`; meetings edit and accounts edit, and never a client seat.
19344
+
19345
+ - Beta: needs the `accounts` and `meetings` feature flag on the workspace.
19346
+ - API: `POST /v1/meetings/{meetingId}/next-step-tasks`
19347
+ - Generated from the API contract
19348
+ - Request body: json (`kenso describe meetings next-step-tasks` prints its schema)
19349
+
19350
+ | Flag | Type | Required | Description |
19351
+ | --- | --- | --- | --- |
19352
+ | `--meeting-id` | string | yes | meetingId (path, string) |
19353
+ | `--data` | json | no | Request body as JSON: inline, @file or - for stdin. Flags win over its keys. |
19354
+ | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
19355
+ | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
19356
+
19357
+ ```bash
19358
+ kenso meetings next-step-tasks --meeting-id <meeting-id>
19359
+ kenso meetings next-step-tasks --meeting-id <meeting-id> --data @body.json
19360
+ ```
19361
+
19241
19362
  ### kenso meetings regenerate-notes
19242
19363
 
19243
19364
  Rewrite the meeting's notes in another template.
@@ -19283,6 +19404,33 @@ An attached source loses its transcript and the Knowledge rows it produced; a di
19283
19404
  kenso meetings remove-source --meeting-id <meeting-id> --source-id <source-id>
19284
19405
  ```
19285
19406
 
19407
+ ### kenso meetings rename-speaker
19408
+
19409
+ Rename a transcript speaker: name them as one of the meeting's participants, or type a name.
19410
+
19411
+ Body `{ "speaker": "Speaker 2", "participantId": 502 }` or `{ "speaker": "Speaker 2", "name": "Jane Doe" }`. `speaker` is the name the transcript shows now (or the label it had before any rename). `participantId` is one of this meeting's participants (`participants[].id` on `GET /v1/meetings/{meetingId}`): their name and email are kept, and the turns match them by email from then on. Otherwise `name` is a typed name, 1 to 120 characters. Neither clears the rename. Every turn of that speaker changes. `source` picks the transcript (`notetaker` or an import's provider such as `fireflies`); without it, the stored transcript the GET returns. The rename reaches every later read (this route, the live view, the MCP, follow-up drafts, call scoring) but is not retroactive: notes already written keep the old names until they are regenerated. While Kenso is still recording, a rename applies to the stored transcript: the answer is that transcript, not the live lines, and a speaker only the live lines have is 422. The meeting's owner or a workspace admin only (403 `forbidden` otherwise); an agent or an API key renames as its member. Returns the transcript as the GET does, with the new names. 400 `validation_failed` for a missing speaker or a bad name; 404 for a meeting you cannot see or a source with no transcript you may read; 422 `validation_error` for a participant not in this meeting or a speaker no turn has; 409 `conflict` when the transcript kept changing (read it again and retry). Flag `meetings`; meetings edit.
19412
+
19413
+ - Beta: needs the `meetings` feature flag on the workspace.
19414
+ - API: `PATCH /v1/meetings/{meetingId}/transcript/speakers`
19415
+ - Generated from the API contract
19416
+ - Request body: json (`kenso describe meetings rename-speaker` prints its schema)
19417
+
19418
+ | Flag | Type | Required | Description |
19419
+ | --- | --- | --- | --- |
19420
+ | `--meeting-id` | string | yes | meetingId (path, string) |
19421
+ | `--speaker` | string | no | speaker (body, string) |
19422
+ | `--name` | string | no | name (body, string) |
19423
+ | `--participant-id` | integer | no | participantId (body, integer) |
19424
+ | `--source` | string | no | source (body, string) |
19425
+ | `--data` | json | no | Request body as JSON: inline, @file or - for stdin. Flags win over its keys. |
19426
+ | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
19427
+ | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
19428
+
19429
+ ```bash
19430
+ kenso meetings rename-speaker --meeting-id <meeting-id>
19431
+ kenso meetings rename-speaker --meeting-id <meeting-id> --data @body.json
19432
+ ```
19433
+
19286
19434
  ### kenso meetings replace-brief
19287
19435
 
19288
19436
  Save a brief you wrote as the meeting's new version.
@@ -19353,9 +19501,9 @@ kenso meetings score-call --meeting-id <meeting-id>
19353
19501
 
19354
19502
  ### kenso meetings update
19355
19503
 
19356
- Move a meeting to another account, clear its account, or make it private or shared.
19504
+ Move a meeting to another account, clear its account, make it private or shared, or choose whether the Kenso Notetaker bot joins.
19357
19505
 
19358
- Body: `accountId` (an account you can see, or null to clear it; else `400`; a meeting with no account gets one only from its owner, else `403`) and `isPrivate` (true or false; only the meeting's owner and workspace admins, else `403`). Absent fields stay unchanged, and an unknown field is `400`. Changing the account moves no stage and stops nothing. Making a meeting private writes one `meeting.made_private` timeline row. `409 conflict` when the meeting kept changing while it was saved: read it again and retry. Returns the meeting.
19506
+ Body: `accountId` (an account you can see, or null to clear it; else `400`; a meeting with no account gets one only from its owner, else `403`), `isPrivate` (true or false; only the meeting's owner and workspace admins, else `403`) and `botMode` (auto, send or skip, else `400`; only the meeting's owner and workspace admins, else `403`; `403` with detail `feature_disabled` while the workspace does not have the Kenso Notetaker bot; skip takes back a bot waiting to join). Absent fields stay unchanged, and an unknown field is `400`. Changing the account moves no stage and stops nothing. Making a meeting private writes one `meeting.made_private` timeline row. `409 conflict` when the meeting kept changing while it was saved: read it again and retry. Returns the meeting with its capture and bot.
19359
19507
 
19360
19508
  - Beta: needs the `meetings` feature flag on the workspace.
19361
19509
  - API: `PATCH /v1/meetings/{meetingId}`
@@ -19367,6 +19515,7 @@ Body: `accountId` (an account you can see, or null to clear it; else `400`; a me
19367
19515
  | `--meeting-id` | string | yes | meetingId (path, string) |
19368
19516
  | `--account-id` | integer | no | accountId (body, integer) |
19369
19517
  | `--is-private` | boolean | no | isPrivate (body, boolean) |
19518
+ | `--bot-mode` | string | no | botMode (body, string) |
19370
19519
  | `--no-is-private` | boolean | no | Send isPrivate as false. |
19371
19520
  | `--data` | json | no | Request body as JSON: inline, @file or - for stdin. Flags win over its keys. |
19372
19521
  | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
@@ -19635,7 +19784,7 @@ kenso members update --member-id <member-id> --data @body.json
19635
19784
 
19636
19785
  Start Microsoft's admin consent for Teams transcripts.
19637
19786
 
19638
- Returns Microsoft's admin-consent page for the Ken Workspace app with the two Teams transcript permissions (`OnlineMeetings.Read` and `OnlineMeetingTranscript.Read.All`), and when the link expires (7 days, since it is often forwarded to the tenant's Microsoft admin). Open it in the browser; a Microsoft admin of the members' tenant approves. Microsoft then sends the browser back to Ken, which lands on `returnTo` with `teams_consent=granted` (members waiting on the approval are searched again), `teams_consent=denied` (declined, or Microsoft failed) or `teams_consent=invalid` (an expired or altered link). Nothing is stored until then. `returnTo` is a dashboard path (at most 512 characters, a single leading `/`); omitted, `/demo/settings/integrations/teams`. `400 validation_failed` for any other `returnTo`, `503 service_unavailable` while Ken's Microsoft app is not configured. Workspace admins only, as a person in Ken: an agent or an API key gets `403`.
19787
+ Returns Microsoft's admin-consent page for the Ken Workspace app with the two Teams transcript permissions (`OnlineMeetings.Read` and `OnlineMeetingTranscript.Read.All`), and when the link expires (7 days, since it is often forwarded to the tenant's Microsoft admin). Open it in the browser; a Microsoft admin of the members' tenant approves. Microsoft then sends the browser back to Ken, which lands on `returnTo` with `teams_consent=granted` (members waiting on the approval are searched again), `teams_consent=denied` (declined, or Microsoft failed) or `teams_consent=invalid` (an expired or altered link). Nothing is stored until then. `returnTo` is a dashboard path (at most 512 characters, a single leading `/`); omitted, `/settings/integrations/teams`. `400 validation_failed` for any other `returnTo`, `503 service_unavailable` while Ken's Microsoft app is not configured. Workspace admins only, as a person in Ken: an agent or an API key gets `403`.
19639
19788
 
19640
19789
  - Beta: needs the `meetings` and `native_transcripts` feature flag on the workspace.
19641
19790
  - API: `POST /v1/integrations/microsoft-teams/admin-consent-sessions`
@@ -19644,7 +19793,7 @@ Returns Microsoft's admin-consent page for the Ken Workspace app with the two Te
19644
19793
 
19645
19794
  | Flag | Type | Required | Description |
19646
19795
  | --- | --- | --- | --- |
19647
- | `--return-to` | string | no | The dashboard path Microsoft's admin-consent page returns to, at most 512 characters, starting with a single `/`. Omitted: `/demo/settings/integrations/teams`. |
19796
+ | `--return-to` | string | no | The dashboard path Microsoft's admin-consent page returns to, at most 512 characters, starting with a single `/`. Omitted: `/settings/integrations/teams`. |
19648
19797
  | `--data` | json | no | Request body as JSON: inline, @file or - for stdin. Flags win over its keys. |
19649
19798
  | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
19650
19799
  | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
@@ -19686,7 +19835,7 @@ kenso migration-connections create --data @body.json
19686
19835
 
19687
19836
  Finish connecting Outreach.
19688
19837
 
19689
- Send the `code`, `state` and `error` Outreach returned, with `provider``outreach`, signed in to the dashboard as the member who started the connect, in the workspace it was started in. `data.returnUrl` is the Kenso app path to land on, `/{workspaceId}/demo/settings/import` with `outreach=connected&connectionId={id}`, or `outreach=error&reason=...` with `provider_error` (declined, or Outreach failed), `exchange_failed`, `scope_required` (a permission was not granted), `key_rejected`, `vendor_error`, `vendor_unavailable`, `not_found`, `run_already_active`, `account_mismatch` (a reconnect of a connection that discovered or ran, with another Outreach organization or one Kenso cannot confirm: add it as a new connection), `discovery_changed` or `save_failed`. A state that is expired, unreadable, or was started by another member or in another workspace is `validation_failed` with `invalid_state` in `error.details[0].code`, and nothing is exchanged. An API key, MCP or agent call is `forbidden`.
19838
+ Send the `code`, `state` and `error` Outreach returned, with `provider``outreach`, signed in to the dashboard as the member who started the connect, in the workspace it was started in. `data.returnUrl` is the Kenso app path to land on, `/{workspaceId}/settings/import` with `outreach=connected&connectionId={id}`, or `outreach=error&reason=...` with `provider_error` (declined, or Outreach failed), `exchange_failed`, `scope_required` (a permission was not granted), `key_rejected`, `vendor_error`, `vendor_unavailable`, `not_found`, `run_already_active`, `account_mismatch` (a reconnect of a connection that discovered or ran, with another Outreach organization or one Kenso cannot confirm: add it as a new connection), `discovery_changed` or `save_failed`. A state that is expired, unreadable, or was started by another member or in another workspace is `validation_failed` with `invalid_state` in `error.details[0].code`, and nothing is exchanged. An API key, MCP or agent call is `forbidden`.
19690
19839
 
19691
19840
  - Beta: needs the `migrations` feature flag on the workspace.
19692
19841
  - API: `POST /v1/migration-connections/connect-completions`
@@ -20052,7 +20201,7 @@ kenso migration-vendors list
20052
20201
 
20053
20202
  Create a model configuration for a campaign in the current workspace.
20054
20203
 
20055
- Returns `201` and `Location`. A campaign outside the workspace is `not_found` before the write. Requires `WriteAccess`.
20204
+ Returns `201` and `Location`. A campaign outside the workspace is `not_found` before the write. Requires Campaigns edit.
20056
20205
 
20057
20206
  - API: `POST /v1/model-configurations`
20058
20207
  - Generated from the API contract
@@ -20080,7 +20229,7 @@ kenso model-configurations create --data @body.json
20080
20229
 
20081
20230
  Delete a model configuration in the current workspace.
20082
20231
 
20083
- Returns `204`. A configuration outside the workspace is `not_found` before the delete. Requires `WriteAccess`.
20232
+ Returns `204`. A configuration outside the workspace is `not_found` before the delete. Requires Campaigns edit.
20084
20233
 
20085
20234
  - API: `DELETE /v1/model-configurations/{modelConfigurationId}`
20086
20235
  - Generated from the API contract
@@ -20099,7 +20248,7 @@ kenso model-configurations delete --model-configuration-id <model-configuration-
20099
20248
 
20100
20249
  Return one model configuration in the current workspace.
20101
20250
 
20102
- A missing id, or one whose campaign is outside the workspace, is `not_found`. Requires `ReadAccess`.
20251
+ A missing id, or one whose campaign is outside the workspace, is `not_found`. Requires Campaigns view.
20103
20252
 
20104
20253
  - API: `GET /v1/model-configurations/{modelConfigurationId}`
20105
20254
  - Generated from the API contract
@@ -20118,7 +20267,7 @@ kenso model-configurations get --model-configuration-id <model-configuration-id>
20118
20267
 
20119
20268
  List model configurations for a campaign in the current workspace.
20120
20269
 
20121
- `campaignId` is required. A campaign outside `X-Ken-Workspace-Id` is `not_found` before the list is read. Optional `flow` narrows the page. Requires `ReadAccess`.
20270
+ `campaignId` is required. A campaign outside `X-Ken-Workspace-Id` is `not_found` before the list is read. Optional `flow` narrows the page. Requires Campaigns view.
20122
20271
 
20123
20272
  - API: `GET /v1/model-configurations` (a list: `--limit`, `--cursor`, `--all`)
20124
20273
  - Generated from the API contract
@@ -20142,7 +20291,7 @@ kenso model-configurations list --all
20142
20291
 
20143
20292
  Update a model configuration in the current workspace.
20144
20293
 
20145
- Absent fields stay unchanged. A configuration outside the workspace is `not_found` before the write. Requires `WriteAccess`.
20294
+ Absent fields stay unchanged. A configuration outside the workspace is `not_found` before the write. Requires Campaigns edit.
20146
20295
 
20147
20296
  - API: `PATCH /v1/model-configurations/{modelConfigurationId}`
20148
20297
  - Generated from the API contract
@@ -20388,7 +20537,7 @@ kenso onboarding get
20388
20537
 
20389
20538
  Update the signed-in user's onboarding progress.
20390
20539
 
20391
- Replaces `PATCH /v1/onboarding/progress`. Null fields are left unchanged. `segment` is `new_to_outbound`, `experienced`, or `expert_ai`. `lane` is `before_launch` or `after_launch`. `answers` is a JSON object that replaces the stored one, at most 262,144 bytes as compact JSON; anything else is `400 validation_failed`. Setting `completedAt` no longer turns `early_access` on: the sample mode is retired (DEV-7366). Saved answers with `mode: recruiting` still turn `recruiting` on for the resolved workspace (DEV-1274). Works before the caller has a workspace. Requires a signed-in user.
20540
+ Replaces `PATCH /v1/onboarding/progress`. Null fields are left unchanged. `segment` is `new_to_outbound`, `experienced`, or `expert_ai`. `lane` is `before_launch` or `after_launch`. `answers` is a JSON object that replaces the stored one, at most 262,144 bytes as compact JSON; anything else is `400 validation_failed`. Setting `completedAt` turns no feature flag on. Saved answers with `mode: recruiting` turn `recruiting` on for the resolved workspace (DEV-1274). Works before the caller has a workspace. Requires a signed-in user.
20392
20541
 
20393
20542
  - API: `PATCH /v1/onboarding`
20394
20543
  - Generated from the API contract
@@ -20899,7 +21048,7 @@ kenso optiverse-connection get
20899
21048
 
20900
21049
  Add up to 500 attendees, exhibitors, speakers or sponsors to an outbound event.
20901
21050
 
20902
- Body: `source` (required: csv, url, manual, api, paste, xlsx, pdf or image, or a Find with Ken source: company_people, prospectus, profile_mention, inbox_mention or last_year), `sourceUrl` (http or https, up to 2048 characters) and `items` (1 to 500). Each item has `role` (required: attendee, exhibitor, speaker or sponsor) and any of `fullName`, `firstName`, `lastName`, `title`, `email`, `linkedinUrl`, `companyName` and `companyDomain` (a domain, website URL or work email; without it, a work email's domain is used), and must name a person (a full name, first and last name, email or LinkedIn URL) or a company. Example: `{ "source": "url", "sourceUrl": "https://example.com/speakers", "items": [ { "role": "speaker", "fullName": "Maya Chen", "title": "VP Sales", "companyName": "Northwind", "companyDomain": "northwind.com" } ] }`. Returns 200 with `succeeded`, `failed` and one result per item in request order: `created` (with `id`), `duplicate` (with the `id` of the row already on the event with the same role and the same email, else LinkedIn URL, else name and company, else company; counted in succeeded) or `validation_failed` (with `error`; counted in failed). A duplicate fills the stored row's blank `title` and `companyDomain` from the item, and a filled domain marks the row matched (source `input`), so resending a row with its domain corrects an unmatched one. Rows with no company domain are looked up in Kenso Search during this call: the company by name, the person by first name, last name and company. A failed lookup marks the row `failed` and never fails the request; sending the row again retries it. Another workspace's event is `404`. Requires `WriteAccess`.
21051
+ Body: `source` (required: csv, url, manual, api, paste, xlsx, pdf or image, or a Find with Ken source: company_people, prospectus, profile_mention, inbox_mention or last_year), `sourceUrl` (http or https, up to 2048 characters) and `items` (1 to 500). Each item has `role` (required: attendee, exhibitor, speaker or sponsor) and any of `fullName`, `firstName`, `lastName`, `title`, `email`, `linkedinUrl`, `companyName` and `companyDomain` (a domain, website URL or work email; without it, a work email's domain is used), and must name a person (a full name, first and last name, email or LinkedIn URL) or a company. Example: `{ "source": "url", "sourceUrl": "https://example.com/speakers", "items": [ { "role": "speaker", "fullName": "Maya Chen", "title": "VP Sales", "companyName": "Northwind", "companyDomain": "northwind.com" } ] }`. Returns 200 with `succeeded`, `failed` and one result per item in request order: `created` (with `id`), `duplicate` (with the `id` of the row already on the event with the same role and the same email, else LinkedIn URL, else name and company, else company; counted in succeeded) or `validation_failed` (with `error`; counted in failed). A duplicate fills the stored row's blank `title` and `companyDomain` from the item, and a filled domain marks the row matched (source `input`), so resending a row with its domain corrects an unmatched one. Rows with no company domain are looked up in Kenso Search during this call: the company by name, the person by first name, last name and company. A failed lookup marks the row `failed` and never fails the request; sending the row again retries it. Another workspace's event is `404`. Requires Accounts edit.
20903
21052
 
20904
21053
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
20905
21054
  - API: `POST /v1/outbound-events/{eventId}/attendees/bulk-create`
@@ -20924,7 +21073,7 @@ kenso outbound-event-attendees bulk-create --event-id <event-id> --data @body.js
20924
21073
 
20925
21074
  Remove one attendee, exhibitor, speaker or sponsor from its event.
20926
21075
 
20927
- Soft delete; returns `204`. The same person or company can be added again afterwards. A row of another workspace, or of a deleted event, is `404 not_found`. Requires `WriteAccess`.
21076
+ Soft delete; returns `204`. The same person or company can be added again afterwards. A row of another workspace, or of a deleted event, is `404 not_found`. Requires Accounts edit.
20928
21077
 
20929
21078
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
20930
21079
  - API: `DELETE /v1/outbound-event-attendees/{attendeeId}`
@@ -20944,7 +21093,7 @@ kenso outbound-event-attendees delete --attendee-id <attendee-id>
20944
21093
 
20945
21094
  List an outbound event's attendees, exhibitors, speakers and sponsors.
20946
21095
 
20947
- Filters: `role` (comma = OR: attendee, exhibitor, speaker, sponsor) and `account` (`matched`: the row's company domain belongs to a live account; `unmatched`: every other row). `limit` 1-500 (default 100) and `cursor` page by id, oldest first. Each row carries `account` (the matched account with its stage and this event's Event touch, else null), `personId` (a person in the workspace with the row's email, else its LinkedIn URL) and `stakeholderId` (that person on that account). With account scope `own`, only accounts the caller owns and their people match. Another workspace's event is `404`. Requires `ReadAccess`.
21096
+ Filters: `role` (comma = OR: attendee, exhibitor, speaker, sponsor) and `account` (`matched`: the row's company domain belongs to a live account; `unmatched`: every other row). `limit` 1-500 (default 100) and `cursor` page by id, oldest first. Each row carries `account` (the matched account with its stage and this event's Event touch, else null), `personId` (a person in the workspace with the row's email, else its LinkedIn URL) and `stakeholderId` (that person on that account). With account scope `own`, only accounts the caller owns and their people match. Another workspace's event is `404`. Requires Accounts view.
20948
21097
 
20949
21098
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
20950
21099
  - API: `GET /v1/outbound-events/{eventId}/attendees` (a list: `--limit`, `--cursor`, `--all`)
@@ -20972,7 +21121,7 @@ kenso outbound-event-attendees list --event-id <event-id> --all
20972
21121
 
20973
21122
  Add a suggested event as an outbound event.
20974
21123
 
20975
- Creates a normal outbound event from the suggestion (name, dates, timezone, format, city, venue and url), ready for an attendee list (`POST /v1/outbound-events/{eventId}/extract-attendees` reads its page), and marks the suggestion `added` with that `eventId`. The body is optional: `{ "attendance": "going" }` (or `not_going`) sets the new event's `attendance`; without it the event is not decided (`null`). Any other attendance is `400 validation_failed`. Returns `200` with `{ suggestion, event }`. A suggestion that is not `suggested` (already added or dismissed) is `409 invalid_state`; another workspace's is `404`. Requires `WriteAccess`.
21124
+ Creates a normal outbound event from the suggestion (name, dates, timezone, format, city, venue and url), ready for an attendee list (`POST /v1/outbound-events/{eventId}/extract-attendees` reads its page), and marks the suggestion `added` with that `eventId`. The body is optional: `{ "attendance": "going" }` (or `not_going`) sets the new event's `attendance`; without it the event is not decided (`null`). Any other attendance is `400 validation_failed`. Returns `200` with `{ suggestion, event }`. A suggestion that is not `suggested` (already added or dismissed) is `409 invalid_state`; another workspace's is `404`. Requires Accounts edit.
20976
21125
 
20977
21126
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
20978
21127
  - API: `POST /v1/outbound-event-suggestions/{suggestionId}/accept`
@@ -20996,7 +21145,7 @@ kenso outbound-event-suggestions accept --suggestion-id <suggestion-id> --data @
20996
21145
 
20997
21146
  Dismiss a suggested event, so Ken never suggests it again.
20998
21147
 
20999
- Marks the suggestion `dismissed` and returns it. Kenso keeps its page and its name and start date, so weekly discovery skips the event on every source. A suggestion that is not `suggested` is `409 invalid_state`; another workspace's is `404`. Requires `WriteAccess`.
21148
+ Marks the suggestion `dismissed` and returns it. Kenso keeps its page and its name and start date, so weekly discovery skips the event on every source. A suggestion that is not `suggested` is `409 invalid_state`; another workspace's is `404`. Requires Accounts edit.
21000
21149
 
21001
21150
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21002
21151
  - API: `POST /v1/outbound-event-suggestions/{suggestionId}/dismiss`
@@ -21016,7 +21165,7 @@ kenso outbound-event-suggestions dismiss --suggestion-id <suggestion-id>
21016
21165
 
21017
21166
  List suggested events in the current workspace, most relevant first.
21018
21167
 
21019
- `status`: `suggested` (default: waiting for a decision; an event that has ended, by today's date in its own `timezone`, else UTC, is left out), `added` or `dismissed`. `limit` 1-200 (default 50) and `cursor` page by `relevance`, then id, both descending. Each suggestion carries `name`, `startsOn`, `endsOn`, `timezone`, `format` (`in_person`, `virtual` or `hybrid`), `city`, `venue`, `url` (the event page), `source` (where Ken found it: `luma`, `dev_events`, `allevents` or `meetup`; `eventbrite`, `tentimes` or `conferenceindex` where Kenso reads them through Firecrawl for the workspace; or `eventbrite` or `linkedin` from a workflow's Apify actor), `sourceUrl` (the listing it was found on; null for an Apify actor's events), `relevance` (0-100), `reason` (one line on why it fits), `accounts` (the workspace's accounts the page names, each `{ accountId, name, role }`), `status`, `eventId` (the event adding it created), `createdAt` and `decidedAt`. Requires `ReadAccess`.
21168
+ `status`: `suggested` (default: waiting for a decision; an event that has ended, by today's date in its own `timezone`, else UTC, is left out), `added` or `dismissed`. `limit` 1-200 (default 50) and `cursor` page by `relevance`, then id, both descending. Each suggestion carries `name`, `startsOn`, `endsOn`, `timezone`, `format` (`in_person`, `virtual` or `hybrid`), `city`, `venue`, `url` (the event page), `source` (where Ken found it: `luma`, `dev_events`, `allevents` or `meetup`; `eventbrite`, `tentimes` or `conferenceindex` where Kenso reads them through Firecrawl for the workspace; or `eventbrite` or `linkedin` from a workflow's Apify actor), `sourceUrl` (the listing it was found on; null for an Apify actor's events), `relevance` (0-100), `reason` (one line on why it fits), `accounts` (the workspace's accounts the page names, each `{ accountId, name, role }`), `status`, `eventId` (the event adding it created), `createdAt` and `decidedAt`. Requires Accounts view.
21020
21169
 
21021
21170
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21022
21171
  - API: `GET /v1/outbound-event-suggestions` (a list: `--limit`, `--cursor`, `--all`)
@@ -21042,7 +21191,7 @@ kenso outbound-event-suggestions list --all
21042
21191
 
21043
21192
  Create an outbound event.
21044
21193
 
21045
- Body: `name` (required, 1-200 characters after trim), `startsOn` (required, YYYY-MM-DD), `endsOn` (YYYY-MM-DD, default `startsOn`, not before it), `timezone` (IANA, such as `America/Chicago`), `format` (`in_person`, the default, `virtual` or `hybrid`), `attendance` (`going` or `not_going`, default null), `city` and `venue` (up to 200 characters) and `url` (http or https, up to 2048 characters). Example: `{ "name": "SaaStr Annual", "startsOn": "2026-09-09", "endsOn": "2026-09-11", "timezone": "America/Los_Angeles", "city": "San Mateo", "url": "https://www.saastrannual.com" }`. Returns `201` with the event. Requires `WriteAccess`.
21194
+ Body: `name` (required, 1-200 characters after trim), `startsOn` (required, YYYY-MM-DD), `endsOn` (YYYY-MM-DD, default `startsOn`, not before it), `timezone` (IANA, such as `America/Chicago`), `format` (`in_person`, the default, `virtual` or `hybrid`), `attendance` (`going` or `not_going`, default null), `city` and `venue` (up to 200 characters) and `url` (http or https, up to 2048 characters). Example: `{ "name": "SaaStr Annual", "startsOn": "2026-09-09", "endsOn": "2026-09-11", "timezone": "America/Los_Angeles", "city": "San Mateo", "url": "https://www.saastrannual.com" }`. Returns `201` with the event. Requires Accounts edit.
21046
21195
 
21047
21196
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21048
21197
  - API: `POST /v1/outbound-events`
@@ -21073,7 +21222,7 @@ kenso outbound-events create --data @body.json
21073
21222
 
21074
21223
  Delete an outbound event.
21075
21224
 
21076
- Soft delete; returns `204`. Its rows stop showing in every list. Another workspace's event is `404`. Requires `WriteAccess`.
21225
+ Soft delete; returns `204`. Its rows stop showing in every list. Another workspace's event is `404`. Requires Accounts edit.
21077
21226
 
21078
21227
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21079
21228
  - API: `DELETE /v1/outbound-events/{eventId}`
@@ -21093,7 +21242,7 @@ kenso outbound-events delete --event-id <event-id>
21093
21242
 
21094
21243
  Read an event page or a pasted list and preview the people or companies it lists.
21095
21244
 
21096
- Body: `url` (the exhibitor, speaker, sponsor or attendee page, an absolute http(s) URL up to 2048 characters) or `text` (a pasted list instead: names, emails, LinkedIn URLs, an email or a page's text, up to 120,000 characters; not both), and `role` (required: attendee, exhibitor, speaker or sponsor, given to every row). Example: `{ "url": "https://example.com/exhibitors", "role": "exhibitor" }` or `{ "text": "Jordan Lee - VP Sales, Acme Robotics", "role": "attendee" }`. A page comes from Kenso's web archive first (`source``archive`), else a live read when the workspace can run one (`live`); otherwise `source` is `none` with no rows. Pasted text is `source``text` with no `url`; a row read from text may also carry an `email`. The model reads the rows, long content in parts of 20,000 characters at once; each row comes back normalized like bulk-create (repeats and rows that fail validation are left out), in order, ready to send to `POST /v1/outbound-events/{eventId}/attendees/bulk-create`. `truncated` is true when the page was too long to read whole, an answer reached the model's output cap (the rows read before it are kept), a part failed while others answered, or the rows reached the cap of 300. Nothing is stored. The model calls are not charged; a live read is charged like any web read of the workspace (an archive read is free). A model failure answers 200 with no rows. Files go to the multipart form of this route (`outbound-events.extract-attendees-from-files`). `mode``prospectus` (DEV-4351; default `attendees`) reads a sponsorship prospectus or a who-attends page for the companies it names (company-only rows, sent to bulk-create with `source``prospectus`), `titleMix` (the job titles it says attendees hold, each with `share` as a fraction or null, largest first, at most 20: send them as `find-company-people`'s `titles`) and `attendeeCount`; a page in this mode is read from the site or Kenso's archive only, never live. Another workspace's event is `404`. Requires `WriteAccess`.
21245
+ Body: `url` (the exhibitor, speaker, sponsor or attendee page, an absolute http(s) URL up to 2048 characters) or `text` (a pasted list instead: names, emails, LinkedIn URLs, an email or a page's text, up to 120,000 characters; not both), and `role` (required: attendee, exhibitor, speaker or sponsor, given to every row). Example: `{ "url": "https://example.com/exhibitors", "role": "exhibitor" }` or `{ "text": "Jordan Lee - VP Sales, Acme Robotics", "role": "attendee" }`. A page comes from Kenso's web archive first (`source``archive`), else a live read when the workspace can run one (`live`); otherwise `source` is `none` with no rows. Pasted text is `source``text` with no `url`; a row read from text may also carry an `email`. The model reads the rows, long content in parts of 20,000 characters at once; each row comes back normalized like bulk-create (repeats and rows that fail validation are left out), in order, ready to send to `POST /v1/outbound-events/{eventId}/attendees/bulk-create`. `truncated` is true when the page was too long to read whole, an answer reached the model's output cap (the rows read before it are kept), a part failed while others answered, or the rows reached the cap of 300. Nothing is stored. The model calls are not charged; a live read is charged like any web read of the workspace (an archive read is free). A model failure answers 200 with no rows. Files go to the multipart form of this route (`outbound-events.extract-attendees-from-files`). `mode``prospectus` (DEV-4351; default `attendees`) reads a sponsorship prospectus or a who-attends page for the companies it names (company-only rows, sent to bulk-create with `source``prospectus`), `titleMix` (the job titles it says attendees hold, each with `share` as a fraction or null, largest first, at most 20: send them as `find-company-people`'s `titles`) and `attendeeCount`; a page in this mode is read from the site or Kenso's archive only, never live. Another workspace's event is `404`. Requires Accounts edit.
21097
21246
 
21098
21247
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21099
21248
  - API: `POST /v1/outbound-events/{eventId}/extract-attendees`
@@ -21120,7 +21269,7 @@ kenso outbound-events extract-attendees --event-id <event-id> --data @body.json
21120
21269
 
21121
21270
  Read uploaded files (a spreadsheet, PDFs, badge or card photos) and preview the people they list.
21122
21271
 
21123
- Multipart form on the same path as `outbound-events.extract-attendees`: `files` (1 to 10 files) and `role` (required: attendee, exhibitor, speaker or sponsor, given to every row). One entry per file comes back in upload order, each with `kind` (`spreadsheet`, `pdf`, `image` or `other`), `status` (`read`, `skipped` or `failed`) and, unless read, a `reason`. An .xlsx (at most 10 MB, one per request; a second is `extra_spreadsheet`) comes back as `table`: its first visible sheet with a value, the first row as `columns` (at most 50) and up to 5,000 `rows`, each with one cell per column, for column mapping; no model reads it. Hidden rows are left out; hidden columns are kept, in place. A PDF (at most 10 MB, the first 40 pages; at most 120,000 characters across the request's PDFs, a later one is `text_limit`) has its text read by the model like a page; one with no text layer, such as a scan, is `no_text`: send photos of its pages instead. Each JPEG, PNG or WebP image (at most 3.75 MB and 8,000 px on a side; a badge, a business card or a list) is one Claude vision call, at most 3 at a time. A PDF's or image's `rows` are normalized like bulk-create, may carry an `email` (never a phone number), and are ready to send to `POST /v1/outbound-events/{eventId}/attendees/bulk-create` with `source``pdf` or `image` (`xlsx` for a mapped table). Other reasons: `unsupported_type` (HEIC included: send it as JPEG), `too_large`, `too_many_files` (past the tenth), `oversized_image` (over 8,000 px on one side: crop it into parts), `unreadable` (a damaged file or image header) and `extraction_failed` (the model did not answer; try again). `truncated` is true when Kenso read only part of a file. `mode``prospectus` (default `attendees`) reads each PDF or image as a sponsorship prospectus or who-attends page: company-only `rows` (bulk-create `source``prospectus`), `titleMix` and `attendeeCount`, as on `outbound-events.extract-attendees`. Nothing is stored. The model calls are not charged. A request over 52 MB is `413`. Another workspace's event is `404`. Requires `WriteAccess`.
21272
+ Multipart form on the same path as `outbound-events.extract-attendees`: `files` (1 to 10 files) and `role` (required: attendee, exhibitor, speaker or sponsor, given to every row). One entry per file comes back in upload order, each with `kind` (`spreadsheet`, `pdf`, `image` or `other`), `status` (`read`, `skipped` or `failed`) and, unless read, a `reason`. An .xlsx (at most 10 MB, one per request; a second is `extra_spreadsheet`) comes back as `table`: its first visible sheet with a value, the first row as `columns` (at most 50) and up to 5,000 `rows`, each with one cell per column, for column mapping; no model reads it. Hidden rows are left out; hidden columns are kept, in place. A PDF (at most 10 MB, the first 40 pages; at most 120,000 characters across the request's PDFs, a later one is `text_limit`) has its text read by the model like a page; one with no text layer, such as a scan, is `no_text`: send photos of its pages instead. Each JPEG, PNG or WebP image (at most 3.75 MB and 8,000 px on a side; a badge, a business card or a list) is one Claude vision call, at most 3 at a time. A PDF's or image's `rows` are normalized like bulk-create, may carry an `email` (never a phone number), and are ready to send to `POST /v1/outbound-events/{eventId}/attendees/bulk-create` with `source``pdf` or `image` (`xlsx` for a mapped table). Other reasons: `unsupported_type` (HEIC included: send it as JPEG), `too_large`, `too_many_files` (past the tenth), `oversized_image` (over 8,000 px on one side: crop it into parts), `unreadable` (a damaged file or image header) and `extraction_failed` (the model did not answer; try again). `truncated` is true when Kenso read only part of a file. `mode``prospectus` (default `attendees`) reads each PDF or image as a sponsorship prospectus or who-attends page: company-only `rows` (bulk-create `source``prospectus`), `titleMix` and `attendeeCount`, as on `outbound-events.extract-attendees`. Nothing is stored. The model calls are not charged. A request over 52 MB is `413`. Another workspace's event is `404`. Requires Accounts edit.
21124
21273
 
21125
21274
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21126
21275
  - API: `POST /v1/outbound-events/{eventId}/extract-attendees`
@@ -21145,7 +21294,7 @@ kenso outbound-events extract-attendees-from-files --event-id <event-id> --file
21145
21294
 
21146
21295
  Find people at the event's exhibitor, sponsor and listed companies with Kenso Search.
21147
21296
 
21148
- Body (optional): `titles` (1 to 20 job titles of up to 100 characters, such as a prospectus's `titleMix`; default the roles that staff a booth: sales, marketing, partnerships, business development, account executives, events and founders; Kenso Search matches a current title that holds every word of one), `nearEventCity` (true: only people whose profile city is the event's city; ignored when the event has none), `perCompany` (3 to 10, default 5) and `limit` (1 to 200, default 200). Example: `{ "titles": ["CFO", "Partner"], "perCompany": 3 }`. The companies are the event's exhibitor and sponsor rows and its rows with no person (the companies a prospectus named), sponsors first, at most 100 per call; each is found in Kenso Search by its domain, else by a name only one company has. People already on the event are left out. Each comes back with `source``company_people`, `isLikely` true (they work there and may not go: label them likely and write "your team is exhibiting", never "saw you're attending"), a `reason` and a `row` with the company's role, ready for `POST /v1/outbound-events/{eventId}/attendees/bulk-create` with that source. `companyCount` is how many companies Kenso Search looked at; `truncated` is true when the limit or the 100-company cap cut the list. A Kenso Search failure leaves its companies out with a `note`. Nothing is stored and nothing is charged; each profile returned counts against the workspace's `searchBrowseRows` quota (`429``quota_exceeded` past it). Another workspace's event is `404`. Requires `WriteAccess`.
21297
+ Body (optional): `titles` (1 to 20 job titles of up to 100 characters, such as a prospectus's `titleMix`; default the roles that staff a booth: sales, marketing, partnerships, business development, account executives, events and founders; Kenso Search matches a current title that holds every word of one), `nearEventCity` (true: only people whose profile city is the event's city; ignored when the event has none), `perCompany` (3 to 10, default 5) and `limit` (1 to 200, default 200). Example: `{ "titles": ["CFO", "Partner"], "perCompany": 3 }`. The companies are the event's exhibitor and sponsor rows and its rows with no person (the companies a prospectus named), sponsors first, at most 100 per call; each is found in Kenso Search by its domain, else by a name only one company has. People already on the event are left out. Each comes back with `source``company_people`, `isLikely` true (they work there and may not go: label them likely and write "your team is exhibiting", never "saw you're attending"), a `reason` and a `row` with the company's role, ready for `POST /v1/outbound-events/{eventId}/attendees/bulk-create` with that source. `companyCount` is how many companies Kenso Search looked at; `truncated` is true when the limit or the 100-company cap cut the list. A Kenso Search failure leaves its companies out with a `note`. Nothing is stored and nothing is charged; each profile returned counts against the workspace's `searchBrowseRows` quota (`429``quota_exceeded` past it). Another workspace's event is `404`. Requires Accounts edit.
21149
21298
 
21150
21299
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21151
21300
  - API: `POST /v1/outbound-events/{eventId}/find-company-people`
@@ -21172,7 +21321,7 @@ kenso outbound-events find-company-people --event-id <event-id> --data @body.jso
21172
21321
 
21173
21322
  Find the previous edition's speaker, sponsor and exhibitor lists and preview their rows, labeled last year.
21174
21323
 
21175
- Body (optional): `url` (the event's site: its home page or a speaker, sponsor or exhibitor page, this year's or last year's, an absolute http(s) URL up to 2048 characters; default the event's `url`, and one of them is required) and `limit` (1 to 500 rows, default 500). Example: `{ "url": "https://kccncna2026.sched.com" }`. Last year is the year before the event's start date (`year`). Ken looks, for free and with no key: first at the link with the year moved back (`kccncna2026.sched.com` is `kccncna2025.sched.com`, an a2z show's `FABTECH2026` is `FABTECH2025`), a past-year page that is often still live; then at the list pages last year's version of the link names, each read live when its address carries last year, else from its latest Wayback Machine copy of last year; with none found, at one Wayback lookup across the site. Common Crawl is not read: its robots.txt closes it to crawlers. At most 4 pages are read (`truncated` when more were found), each with the extract-attendees readers: a platform's own list (Sched, Sessionize, Map Your Show, a2z, ExpoFP, Luma) with no model call, any other page's text by the model. `lists` has each page read: `url` (as published), `archiveUrl` and `capturedAt` (the Wayback copy, null when live), `role`, `reader`, `count` (rows it gave), `truncated` and `note`. Each of `people` has `source``last_year`, `lastYear` true, `isLikely` true, a `reason` (<c>Speaker in 2025 on kccncna2025.sched.com</c>), `pageUrl`, `capturedAt` and a `row` with its role on last year's list, ready for `POST /v1/outbound-events/{eventId}/attendees/bulk-create` with source `last_year`, which labels the row "last year" on the event. Rows already on the event in the same role are left out. Last year's list is a proxy: companies usually come back (63% of FABTECH 2026's exhibitors were there in 2025), people less often (31% of KubeCon NA 2025's speakers spoke in 2024), so write "spoke last year", never "see you there". `truncated` is true when the limit, the page cap or a list's own cap cut the answer. `note` says why nothing came back. Nothing is stored and nothing is charged. Another workspace's event is `404`. Requires `WriteAccess`.
21324
+ Body (optional): `url` (the event's site: its home page or a speaker, sponsor or exhibitor page, this year's or last year's, an absolute http(s) URL up to 2048 characters; default the event's `url`, and one of them is required) and `limit` (1 to 500 rows, default 500). Example: `{ "url": "https://kccncna2026.sched.com" }`. Last year is the year before the event's start date (`year`). Ken looks, for free and with no key: first at the link with the year moved back (`kccncna2026.sched.com` is `kccncna2025.sched.com`, an a2z show's `FABTECH2026` is `FABTECH2025`), a past-year page that is often still live; then at the list pages last year's version of the link names, each read live when its address carries last year, else from its latest Wayback Machine copy of last year; with none found, at one Wayback lookup across the site. Common Crawl is not read: its robots.txt closes it to crawlers. At most 4 pages are read (`truncated` when more were found), each with the extract-attendees readers: a platform's own list (Sched, Sessionize, Map Your Show, a2z, ExpoFP, Luma) with no model call, any other page's text by the model. `lists` has each page read: `url` (as published), `archiveUrl` and `capturedAt` (the Wayback copy, null when live), `role`, `reader`, `count` (rows it gave), `truncated` and `note`. Each of `people` has `source``last_year`, `lastYear` true, `isLikely` true, a `reason` (<c>Speaker in 2025 on kccncna2025.sched.com</c>), `pageUrl`, `capturedAt` and a `row` with its role on last year's list, ready for `POST /v1/outbound-events/{eventId}/attendees/bulk-create` with source `last_year`, which labels the row "last year" on the event. Rows already on the event in the same role are left out. Last year's list is a proxy: companies usually come back (63% of FABTECH 2026's exhibitors were there in 2025), people less often (31% of KubeCon NA 2025's speakers spoke in 2024), so write "spoke last year", never "see you there". `truncated` is true when the limit, the page cap or a list's own cap cut the answer. `note` says why nothing came back. Nothing is stored and nothing is charged. Another workspace's event is `404`. Requires Accounts edit.
21176
21325
 
21177
21326
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21178
21327
  - API: `POST /v1/outbound-events/{eventId}/find-last-year`
@@ -21197,7 +21346,7 @@ kenso outbound-events find-last-year --event-id <event-id> --data @body.json
21197
21346
 
21198
21347
  Find people who say they are going: inbox replies and Kenso Search profiles that name the event.
21199
21348
 
21200
- Body (optional): `name` (the event name to look for, up to 100 characters; default the event's name without its year, its city or a part in brackets: `Web Summit Lisbon 2026` is `Web Summit`) and `limit` (1 to 100, default 50). First come inbound campaign replies in this workspace's inbox (not bounces or automated replies) from the year before the event until it ends whose own words (the quoted email cut off) name it as somewhere the writer goes ("see you at Slush", or the name before the event's year) and do not decline it ("I won't be at Slush"), with `source``inbox_mention`, the reply's `contactId`, `threadId` and `repliedAt`. Then Kenso Search profiles whose headline, summary or current title name it (`source``profile_mention`; not the organizer's staff, nor a profile naming only a past edition such as "Speaker at Slush 2019"): those naming it with the event's year first, then those naming it alone, unless the name alone is on more than 2,000 profiles (common words), when only the year matches count (`phrase` says what was looked for). LinkedIn posts are not in Kenso Search. `isLikely` is false: they named the event themselves. Every `row` is an attendee, ready for `POST /v1/outbound-events/{eventId}/attendees/bulk-create` with its source; people already on the event are left out. Nothing is stored and nothing is charged; each profile returned counts against the workspace's `searchBrowseRows` quota. Another workspace's event is `404`. Requires `WriteAccess`.
21349
+ Body (optional): `name` (the event name to look for, up to 100 characters; default the event's name without its year, its city or a part in brackets: `Web Summit Lisbon 2026` is `Web Summit`) and `limit` (1 to 100, default 50). First come inbound campaign replies in this workspace's inbox (not bounces or automated replies) from the year before the event until it ends whose own words (the quoted email cut off) name it as somewhere the writer goes ("see you at Slush", or the name before the event's year) and do not decline it ("I won't be at Slush"), with `source``inbox_mention`, the reply's `contactId`, `threadId` and `repliedAt`. Then Kenso Search profiles whose headline, summary or current title name it (`source``profile_mention`; not the organizer's staff, nor a profile naming only a past edition such as "Speaker at Slush 2019"): those naming it with the event's year first, then those naming it alone, unless the name alone is on more than 2,000 profiles (common words), when only the year matches count (`phrase` says what was looked for). LinkedIn posts are not in Kenso Search. `isLikely` is false: they named the event themselves. Every `row` is an attendee, ready for `POST /v1/outbound-events/{eventId}/attendees/bulk-create` with its source; people already on the event are left out. Nothing is stored and nothing is charged; each profile returned counts against the workspace's `searchBrowseRows` quota. Another workspace's event is `404`. Requires Accounts edit.
21201
21350
 
21202
21351
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21203
21352
  - API: `POST /v1/outbound-events/{eventId}/find-people-going`
@@ -21222,7 +21371,7 @@ kenso outbound-events find-people-going --event-id <event-id> --data @body.json
21222
21371
 
21223
21372
  Return one outbound event with its counts.
21224
21373
 
21225
- Another workspace's event, or a deleted one, is `404 not_found`. Requires `ReadAccess`.
21374
+ Another workspace's event, or a deleted one, is `404 not_found`. Requires Accounts view.
21226
21375
 
21227
21376
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21228
21377
  - API: `GET /v1/outbound-events/{eventId}`
@@ -21242,7 +21391,7 @@ kenso outbound-events get --event-id <event-id>
21242
21391
 
21243
21392
  Show the LinkedIn attendee reads of an outbound event: the warning, the limits, your seats and the latest reads.
21244
21393
 
21245
- Returns `enabled` (false while reads are switched off: the warning and limits still show, and no read can start), `warning` (to show before every read), `note` (what a read can and cannot do), `limits`, `event` (attendees counted against the event's limit, attendees left, and `nextReadAt` while the gap between reads runs), `seats` (the LinkedIn seats the caller connected here, each with `canRead`, `allowance`, and when it cannot read, `refusal`: seat_unavailable, seat_paused, event_cooldown, event_limit, seat_reads_today or seat_limit, with `message` and `availableAt`) and `reads` (the latest 5, newest first, each with `status` queued, reading, done or failed, `stopReason`, `message` and its preview `rows`, normalized like bulk-create). Another workspace's event is `404`. Requires `ReadAccess`.
21394
+ Returns `enabled` (false while reads are switched off: the warning and limits still show, and no read can start), `warning` (to show before every read), `note` (what a read can and cannot do), `limits`, `event` (attendees counted against the event's limit, attendees left, and `nextReadAt` while the gap between reads runs), `seats` (the LinkedIn seats the caller connected here, each with `canRead`, `allowance`, and when it cannot read, `refusal`: seat_unavailable, seat_paused, event_cooldown, event_limit, seat_reads_today or seat_limit, with `message` and `availableAt`) and `reads` (the latest 5, newest first, each with `status` queued, reading, done or failed, `stopReason`, `message` and its preview `rows`, normalized like bulk-create). Another workspace's event is `404`. Requires Accounts view.
21246
21395
 
21247
21396
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21248
21397
  - API: `GET /v1/outbound-events/{eventId}/linkedin-attendees`
@@ -21262,7 +21411,7 @@ kenso outbound-events linkedin-attendees --event-id <event-id>
21262
21411
 
21263
21412
  List outbound events in the current workspace, newest first.
21264
21413
 
21265
- `status`: `upcoming` (the event ends today or later, today being the date in the event's own `timezone`, else UTC), `past` (it ended before today) or `all` (default). `limit` 1-200 (default 50) and `cursor` page by id, newest first. Each event carries its `format`, `attendance` (`going`, `not_going` or null), its `status` (`upcoming`, `live` or `past`, from its dates in its own `timezone`, else UTC) and `counts`: live rows, rows per role, distinct companies and distinct live accounts whose domain a row carries (with account scope `own`, only accounts the caller owns). Requires `ReadAccess`.
21414
+ `status`: `upcoming` (the event ends today or later, today being the date in the event's own `timezone`, else UTC), `past` (it ended before today) or `all` (default). `limit` 1-200 (default 50) and `cursor` page by id, newest first. Each event carries its `format`, `attendance` (`going`, `not_going` or null), its `status` (`upcoming`, `live` or `past`, from its dates in its own `timezone`, else UTC) and `counts`: live rows, rows per role, distinct companies and distinct live accounts whose domain a row carries (with account scope `own`, only accounts the caller owns). Requires Accounts view.
21266
21415
 
21267
21416
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21268
21417
  - API: `GET /v1/outbound-events` (a list: `--limit`, `--cursor`, `--all`)
@@ -21286,7 +21435,7 @@ kenso outbound-events list --all
21286
21435
 
21287
21436
  Plan an Event touch for every target account attending the event.
21288
21437
 
21289
- Body (optional): `dueAt` (ISO 8601; default 09:00 UTC fourteen days before the event starts, or now when that has passed). Example: `{ "dueAt": "2026-10-21T09:00:00Z" }`. Every live account an attendee matches by company domain gets one touch on the `event` channel titled `Meet at {event name}`, with a body naming the event and the account's people on the list, on the stakeholder of its first listed person (speakers first). The touches wait for a person to approve them in Today; approving one marks it done and sends nothing. Accounts in stage `paused`, `not_a_fit` or `lost`, and touches a Contact limits guardrail refuses, are skipped with the reason. A second call finds the touches it made (`touchesExisting`). Then the event's speakers at those accounts become `event` account signals (`signalsCreated`, new ones only). An event that ended before today in its `timezone` (UTC when it has none), so its status is `past`, is `409 invalid_state`. Another workspace's event is `404`. Requires `WriteAccess`.
21438
+ Body (optional): `dueAt` (ISO 8601; default 09:00 UTC fourteen days before the event starts, or now when that has passed). Example: `{ "dueAt": "2026-10-21T09:00:00Z" }`. Every live account an attendee matches by company domain gets one touch on the `event` channel titled `Meet at {event name}`, with a body naming the event and the account's people on the list, on the stakeholder of its first listed person (speakers first). The touches wait for a person to approve them in Today; approving one marks it done and sends nothing. Accounts in stage `paused`, `not_a_fit` or `lost`, and touches a Contact limits guardrail refuses, are skipped with the reason. A second call finds the touches it made (`touchesExisting`). Then the event's speakers at those accounts become `event` account signals (`signalsCreated`, new ones only). An event that ended before today in its `timezone` (UTC when it has none), so its status is `past`, is `409 invalid_state`. Another workspace's event is `404`. Requires Accounts edit.
21290
21439
 
21291
21440
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21292
21441
  - API: `POST /v1/outbound-events/{eventId}/plan-touches`
@@ -21310,7 +21459,7 @@ kenso outbound-events plan-touches --event-id <event-id> --data @body.json
21310
21459
 
21311
21460
  Read an event's page and preview the event fields it states, to fill the New event form.
21312
21461
 
21313
- Body: `url` (required: the event's page, an absolute http(s) URL up to 2048 characters, such as a Luma, Eventbrite or Sched link or the conference's own site). Example: `{ "url": "https://lu.ma/revops-mixer" }`. The page is read straight from the site (robots.txt obeyed, never Firecrawl). Each field comes from the first source that states it: the page's schema.org Event (`json_ld`), then Luma's or Eventbrite's own page data (`luma`, `eventbrite`, which add the IANA `timezone`); only when neither names an event does one Claude Haiku call read the page's text (`model`; not charged). Returns 200 with `url` (the page that answered), `platform` (`luma`, `eventbrite`, `sched` or null), `event` (`name`, `startsOn`, `endsOn`, `timezone`, `format`, `city`, `venue` and `url`, the body `outbound-events.create` takes; a field the page does not state is absent), `sources` (per field, where it came from), `missing` (the fields to fill in by hand; no city or venue for a virtual event), `pages` (the site's speaker, sponsor, exhibitor and attendee pages, each with `url`, `role` and `label`, to read with `outbound-events.extract-attendees` once the event exists; a Luma event offers its hosts and featured guests) and `note` (why the page could not be read or named no event, else absent). A page that cannot be read answers 200 with only `event.url` and the note. Nothing is stored. Each call counts on the workspace's `outboundEventLinkPreviews` quota (30 an hour, 200 a day; past it, `429 quota_exceeded` with `Retry-After`). Requires `WriteAccess`.
21462
+ Body: `url` (required: the event's page, an absolute http(s) URL up to 2048 characters, such as a Luma, Eventbrite or Sched link or the conference's own site). Example: `{ "url": "https://lu.ma/revops-mixer" }`. The page is read straight from the site (robots.txt obeyed, never Firecrawl). Each field comes from the first source that states it: the page's schema.org Event (`json_ld`), then Luma's or Eventbrite's own page data (`luma`, `eventbrite`, which add the IANA `timezone`); only when neither names an event does one Claude Haiku call read the page's text (`model`; not charged). Returns 200 with `url` (the page that answered), `platform` (`luma`, `eventbrite`, `sched` or null), `event` (`name`, `startsOn`, `endsOn`, `timezone`, `format`, `city`, `venue` and `url`, the body `outbound-events.create` takes; a field the page does not state is absent), `sources` (per field, where it came from), `missing` (the fields to fill in by hand; no city or venue for a virtual event), `pages` (the site's speaker, sponsor, exhibitor and attendee pages, each with `url`, `role` and `label`, to read with `outbound-events.extract-attendees` once the event exists; a Luma event offers its hosts and featured guests) and `note` (why the page could not be read or named no event, else absent). A page that cannot be read answers 200 with only `event.url` and the note. Nothing is stored. Each call counts on the workspace's `outboundEventLinkPreviews` quota (30 an hour, 200 a day; past it, `429 quota_exceeded` with `Retry-After`). Requires Accounts edit.
21314
21463
 
21315
21464
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21316
21465
  - API: `POST /v1/outbound-events/preview-from-link`
@@ -21333,7 +21482,7 @@ kenso outbound-events preview-from-link --data @body.json
21333
21482
 
21334
21483
  Start reading a LinkedIn event's attendees through one of your seats.
21335
21484
 
21336
- Body: `url` (required: the event's page, `https://www.linkedin.com/events/...`, or its attendee search link), `linkedInAccountId` (required: a LinkedIn seat the caller connected in this workspace, connected and not paused) and `acceptWarning` (required, true: the caller read the warning from `GET /v1/outbound-events/{eventId}/linkedin-attendees`). Example: `{ "url": "https://www.linkedin.com/events/7212345678901234567/", "linkedInAccountId": 6, "acceptWarning": true }`. Automated reads break LinkedIn's User Agreement (section 8.2) and can get the seat restricted. The seat must already attend the event on LinkedIn; Ken never clicks Attend. Returns `202` with the queued read. Ken.EventProcesor reads the seat's classic people search filtered by the event, 10 people a page, at least 30 seconds apart, within the seat's search budget, and keeps the people as the read's preview: nothing is added to the event until you send the rows you keep to bulk-create. Hard limits: 100 attendees a read; one read of an event every 24 hours and 300 attendees per event; 2 reads and 200 attendees per seat per UTC day. The first rate limit or restriction sign stops the read and pauses event reads on the seat for 72 hours. An empty first page ends the read with `no_attendees`: the seat is not attending, or LinkedIn does not list attendees to it. Errors: `400 validation_failed` for a missing or non-event `url` (`invalid_linkedin_event_url`), a missing seat, a seat that is not yours or not usable (`linkedin_account_unavailable`) or `acceptWarning` not true (`warning_not_accepted`); `409 invalid_state` when reads are switched off (`switched_off`) or a limit refuses the read (the detail code is the limit, with a message saying when it lifts). Another workspace's event is `404`. Requires `WriteAccess`.
21485
+ Body: `url` (required: the event's page, `https://www.linkedin.com/events/...`, or its attendee search link), `linkedInAccountId` (required: a LinkedIn seat the caller connected in this workspace, connected and not paused) and `acceptWarning` (required, true: the caller read the warning from `GET /v1/outbound-events/{eventId}/linkedin-attendees`). Example: `{ "url": "https://www.linkedin.com/events/7212345678901234567/", "linkedInAccountId": 6, "acceptWarning": true }`. Automated reads break LinkedIn's User Agreement (section 8.2) and can get the seat restricted. The seat must already attend the event on LinkedIn; Ken never clicks Attend. Returns `202` with the queued read. Ken.EventProcesor reads the seat's classic people search filtered by the event, 10 people a page, at least 30 seconds apart, within the seat's search budget, and keeps the people as the read's preview: nothing is added to the event until you send the rows you keep to bulk-create. Hard limits: 100 attendees a read; one read of an event every 24 hours and 300 attendees per event; 2 reads and 200 attendees per seat per UTC day. The first rate limit or restriction sign stops the read and pauses event reads on the seat for 72 hours. An empty first page ends the read with `no_attendees`: the seat is not attending, or LinkedIn does not list attendees to it. Errors: `400 validation_failed` for a missing or non-event `url` (`invalid_linkedin_event_url`), a missing seat, a seat that is not yours or not usable (`linkedin_account_unavailable`) or `acceptWarning` not true (`warning_not_accepted`); `409 invalid_state` when reads are switched off (`switched_off`) or a limit refuses the read (the detail code is the limit, with a message saying when it lifts). Another workspace's event is `404`. Requires Accounts edit.
21337
21486
 
21338
21487
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21339
21488
  - API: `POST /v1/outbound-events/{eventId}/read-linkedin-attendees`
@@ -21360,7 +21509,7 @@ kenso outbound-events read-linkedin-attendees --event-id <event-id> --data @body
21360
21509
 
21361
21510
  Update an outbound event. Absent fields stay unchanged.
21362
21511
 
21363
- `name`, `startsOn` and `format` cannot be null. `endsOn` null resets it to `startsOn`; the end cannot be before the start. `attendance`, `timezone`, `city`, `venue` and `url` accept null to clear. Same limits as create. Example: `{ "endsOn": "2026-09-12", "venue": null }`. Another workspace's event is `404`. Requires `WriteAccess`.
21512
+ `name`, `startsOn` and `format` cannot be null. `endsOn` null resets it to `startsOn`; the end cannot be before the start. `attendance`, `timezone`, `city`, `venue` and `url` accept null to clear. Same limits as create. Example: `{ "endsOn": "2026-09-12", "venue": null }`. Another workspace's event is `404`. Requires Accounts edit.
21364
21513
 
21365
21514
  - Beta: needs the `accounts` and `event_outbound` feature flag on the workspace.
21366
21515
  - API: `PATCH /v1/outbound-events/{eventId}`
@@ -21394,7 +21543,7 @@ kenso outbound-events update --event-id <event-id> --data @body.json
21394
21543
 
21395
21544
  Return what people see on one domain: its website mode and each hostname's page status.
21396
21545
 
21397
- `mode` is `unset` (nobody answered: the domain keeps its redirect), `redirect` or `page`. `currentVersionId` is the workspace's live page version, null until the first publish. `hostnames` lists the apex first, then `www`, each with `attachPath` (`worker_route`, `custom_hostname` or null when unsupported), `dnsWriter`, `status` (`detached`, `attaching`, `active`, `detaching` or `failed`), `sslStatus`, `unsupportedReason` and `checkedAt`; empty until Kenso has checked the domain. Requires `ReadAccess` and the `domain_pages` feature.
21546
+ `mode` is `unset` (nobody answered: the domain keeps its redirect), `redirect` or `page`. `currentVersionId` is the workspace's live page version, null until the first publish. `hostnames` lists the apex first, then `www`, each with `attachPath` (`worker_route`, `custom_hostname` or null when unsupported), `dnsWriter`, `status` (`detached`, `attaching`, `active`, `detaching` or `failed`), `sslStatus`, `unsupportedReason` and `checkedAt`; empty until Kenso has checked the domain. Requires Delivery view and the `domain_pages` feature.
21398
21547
 
21399
21548
  - Beta: needs the `domain_pages` feature flag on the workspace.
21400
21549
  - API: `GET /v1/domains/{domainId}/website`
@@ -21546,7 +21695,7 @@ kenso outbound-site versions-restore --version-id <version-id>
21546
21695
 
21547
21696
  Return a person's activity timeline: what is planned next and what already happened.
21548
21697
 
21549
- The same rows, parameters and shape as `accounts.activity`, for one person: their touches on the accounts the caller can see, the activity events naming them, and every email and LinkedIn message to or from them (`message_sent` and `message_received` rows with `message`). An account's timeline is the union of its people's. As there, message text comes only to a caller who may view the Inbox. `category` (comma = OR) and `limit` (1-500, default 100) work as there. An unknown or deleted person, another workspace's, or with account scope `own` a person on none of the caller's accounts, is `404 not_found`. A merged person is `404` with detail `person_merged` and `mergedIntoId`. Requires `ReadAccess`.
21698
+ The same rows, parameters and shape as `accounts.activity`, for one person: their touches on the accounts the caller can see, the activity events naming them, and every email and LinkedIn message to or from them (`message_sent` and `message_received` rows with `message`). An account's timeline is the union of its people's. As there, message text comes only to a caller who may view the Inbox. `category` (comma = OR) and `limit` (1-500, default 100) work as there. An unknown or deleted person, another workspace's, or with account scope `own` a person on none of the caller's accounts, is `404 not_found`. A merged person is `404` with detail `person_merged` and `mergedIntoId`. Requires Accounts view.
21550
21699
 
21551
21700
  - Beta: needs the `accounts` feature flag on the workspace.
21552
21701
  - API: `GET /v1/people/{personId}/activity`
@@ -21568,7 +21717,7 @@ kenso people activity --person-id <person-id>
21568
21717
 
21569
21718
  Create or match up to 200 people, each optionally onto an account.
21570
21719
 
21571
- Body: `rows` (1-200) of `{ person | personId, accountId | accountDomain, role, priority }`, and `findEmails`. Each row runs in its own transaction: an invalid row writes nothing and comes back `invalid` with an `error`; the rest go on. A role address (support@, info@, no-reply@) is not the person's own email: it matches the person who holds it and never fills onto one. A new person with a LinkedIn URL or Kenso person id is created without it; one with a real first and last name (as on `people.create`) matches the same full name on the row's account, else is created there without it, and without an account is `invalid`, because only the name would find them again. Those rows carry `emailDropped: role_address`. A row with only the address is `invalid`; both invalid cases carry `reason: role_address`. A person with the same email, else LinkedIn URL, else Kenso person id, is `matched` and only their blank fields fill; a row with none of them matches the same full name on the same account; else the person is `created`. A row with only a name needs a live account, else it is `invalid`. `accountDomain` resolves to the live account with that registrable domain (unknown: `account_not_found`; a person with an email, LinkedIn URL or Kenso person id is still written). A batch never creates accounts. Places are user writes (`source: csv`), so role and priority pin; a place a person removed is reported `previously_removed`, never forced. Re-sending the same rows creates nothing. `findEmails: true` queues a find for each created or matched person without an email, and `paused` is true when they wait for credits; if queueing fails, the rows still come back with `emailFind: not_requested`. With account scope `own`, every row must name one of the caller's accounts, and a `personId` must be a person they can see; other rows are `invalid`. A match on none of their accounts that comes back `previously_removed` is left untouched and has no `personId`, and finds go only to people on one of their accounts. Requires `WriteAccess`.
21720
+ Body: `rows` (1-200) of `{ person | personId, accountId | accountDomain, role, priority }`, and `findEmails`. Each row runs in its own transaction: an invalid row writes nothing and comes back `invalid` with an `error`; the rest go on. A role address (support@, info@, no-reply@) is not the person's own email: it matches the person who holds it and never fills onto one. A new person with a LinkedIn URL or Kenso person id is created without it; one with a real first and last name (as on `people.create`) matches the same full name on the row's account, else is created there without it, and without an account is `invalid`, because only the name would find them again. Those rows carry `emailDropped: role_address`. A row with only the address is `invalid`; both invalid cases carry `reason: role_address`. A person with the same email, else LinkedIn URL, else Kenso person id, is `matched` and only their blank fields fill; a row with none of them matches the same full name on the same account; else the person is `created`. A row with only a name needs a live account, else it is `invalid`. `accountDomain` resolves to the live account with that registrable domain (unknown: `account_not_found`; a person with an email, LinkedIn URL or Kenso person id is still written). A batch never creates accounts. Places are user writes (`source: csv`), so role and priority pin; a place a person removed is reported `previously_removed`, never forced. Re-sending the same rows creates nothing. `findEmails: true` queues a find for each created or matched person without an email, and `paused` is true when they wait for credits; if queueing fails, the rows still come back with `emailFind: not_requested`. With account scope `own`, every row must name one of the caller's accounts, and a `personId` must be a person they can see; other rows are `invalid`. A match on none of their accounts that comes back `previously_removed` is left untouched and has no `personId`, and finds go only to people on one of their accounts. Requires Accounts edit.
21572
21721
 
21573
21722
  - Beta: needs the `accounts` feature flag on the workspace.
21574
21723
  - API: `POST /v1/people/batch`
@@ -21592,7 +21741,7 @@ kenso people batch --data @body.json
21592
21741
 
21593
21742
  Create one person on no account.
21594
21743
 
21595
- Body: `fullName` (required), `firstName`, `lastName`, `title`, `email`, `linkedinUrl`, `kenPersonId`, `ownerMemberId`, `leadStatus`, `phones` (as on `people.update`). The first and latest source are `manual` for a person and `api` for an API key or agent. Email is trimmed and lower-cased; the LinkedIn URL is normalized like the `linkedinUrl` filter. A live person with the same email or LinkedIn URL is `409 conflict` with detail `person_exists` and their `personId`: use them instead of creating a twin. A name-only match is not a duplicate. A role address (a shared mailbox such as support@, info@, no-reply@) is not a person: a person who already holds it is still `409 person_exists`. Else with a `linkedinUrl`, a `kenPersonId` or a real first and last name (both parts, from `firstName` and `lastName` or `fullName`, neither a role word such as support or team, the address's local part or a label of its domain, nor an initial only; intake also checks the company name it is sent) the person is created without it and the answer carries `emailDropped: role_address`; with none of them it is `422 role_address`. No email find starts. Returns `201` with `accounts: []`. With account scope `own` it is `403``permission_denied` with `scope: all`: add the person to one of your accounts instead. Requires `WriteAccess`.
21744
+ Body: `fullName` (required), `firstName`, `lastName`, `title`, `email`, `linkedinUrl`, `kenPersonId`, `ownerMemberId`, `leadStatus`, `phones` (as on `people.update`). The first and latest source are `manual` for a person and `api` for an API key or agent. Email is trimmed and lower-cased; the LinkedIn URL is normalized like the `linkedinUrl` filter. A live person with the same email or LinkedIn URL is `409 conflict` with detail `person_exists` and their `personId`: use them instead of creating a twin. A name-only match is not a duplicate. A role address (a shared mailbox such as support@, info@, no-reply@) is not a person: a person who already holds it is still `409 person_exists`. Else with a `linkedinUrl`, a `kenPersonId` or a real first and last name (both parts, from `firstName` and `lastName` or `fullName`, neither a role word such as support or team, the address's local part or a label of its domain, nor an initial only; intake also checks the company name it is sent) the person is created without it and the answer carries `emailDropped: role_address`; with none of them it is `422 role_address`. No email find starts. Returns `201` with `accounts: []`. With account scope `own` it is `403``permission_denied` with `scope: all`: add the person to one of your accounts instead. Requires Accounts edit.
21596
21745
 
21597
21746
  - Beta: needs the `accounts` feature flag on the workspace.
21598
21747
  - API: `POST /v1/people`
@@ -21623,7 +21772,7 @@ kenso people create --data @body.json
21623
21772
 
21624
21773
  Delete a person who is on no live account.
21625
21774
 
21626
- Soft delete: the person leaves every list, search and find-or-create match, so the same email can be added again as a new person, and Kenso never re-adds them. A person still on an account is `409 conflict` with detail `person_on_accounts` and `accounts` (`accountId`, `name`, `stakeholderId`): remove them from each first. Cancels their active email find. Returns `204`. With account scope `own` it is `403``permission_denied` with `scope: all`. Requires `WriteAccess`.
21775
+ Soft delete: the person leaves every list, search and find-or-create match, so the same email can be added again as a new person, and Kenso never re-adds them. A person still on an account is `409 conflict` with detail `person_on_accounts` and `accounts` (`accountId`, `name`, `stakeholderId`): remove them from each first. Cancels their active email find. Returns `204`. With account scope `own` it is `403``permission_denied` with `scope: all`. Requires Accounts edit.
21627
21776
 
21628
21777
  - Beta: needs the `accounts` feature flag on the workspace.
21629
21778
  - API: `DELETE /v1/people/{personId}`
@@ -21643,7 +21792,7 @@ kenso people delete --person-id <person-id>
21643
21792
 
21644
21793
  Remove the photo uploaded for a person.
21645
21794
 
21646
- The person shows their LinkedIn photo again (`photoSource: linkedin`) when Kenso has one, else no photo. Removing a photo the person does not have changes nothing. A merged person is `404``person_merged`. Returns the person. Requires `WriteAccess`.
21795
+ The person shows their LinkedIn photo again (`photoSource: linkedin`) when Kenso has one, else no photo. Removing a photo the person does not have changes nothing. A merged person is `404``person_merged`. Returns the person. Requires Accounts edit.
21647
21796
 
21648
21797
  - Beta: needs the `accounts` feature flag on the workspace.
21649
21798
  - API: `DELETE /v1/people/{personId}/photo`
@@ -21663,7 +21812,7 @@ kenso people delete-photo --person-id <person-id>
21663
21812
 
21664
21813
  List the open merge suggestions for this person.
21665
21814
 
21666
- Ken suggests that two people may be one human when lead intake creates a person with the same name on the email domain of a person who has another email, or when the nightly sweep finds the same email apart from dots and plus tags, the same phone, the same name on the same account, or the same name and email domain. Each item carries `id`, `todayItemId` (`merge_suggestion_{id}`), `personId`, `otherPersonId`, `reasons` (`email_local_part`, `phone`, `name_account`, `name_domain`), `foundBy` (`intake` or `sweep`), `createdAt` and `otherPerson` (the person summary), newest first. Merge with `people.merge`, which closes the suggestion; "Not a duplicate" is <c>PATCH /v1/today/items/{todayItemId}</c> with `state: done`, and Ken never suggests that pair again. A merged person is `404``person_merged`. With account scope `own`, suggestions whose other person is on none of the caller's accounts are left out. Behind the `lead_intake` feature (404 when off). Requires `ReadAccess`.
21815
+ Ken suggests that two people may be one human when lead intake creates a person with the same name on the email domain of a person who has another email, or when the nightly sweep finds the same email apart from dots and plus tags, the same phone, the same name on the same account, or the same name and email domain. Each item carries `id`, `todayItemId` (`merge_suggestion_{id}`), `personId`, `otherPersonId`, `reasons` (`email_local_part`, `phone`, `name_account`, `name_domain`), `foundBy` (`intake` or `sweep`), `createdAt` and `otherPerson` (the person summary), newest first. Merge with `people.merge`, which closes the suggestion; "Not a duplicate" is <c>PATCH /v1/today/items/{todayItemId}</c> with `state: done`, and Ken never suggests that pair again. A merged person is `404``person_merged`. With account scope `own`, suggestions whose other person is on none of the caller's accounts are left out. Behind the `lead_intake` feature (404 when off). Requires Accounts view.
21667
21816
 
21668
21817
  - Beta: needs the `accounts` and `lead_intake` feature flag on the workspace.
21669
21818
  - API: `GET /v1/people/{personId}/duplicates`
@@ -21683,7 +21832,7 @@ kenso people duplicates --person-id <person-id>
21683
21832
 
21684
21833
  Export the people the filters match as CSV.
21685
21834
 
21686
- Body: the `people.search` filters and `sort`, no paging. Answers a streamed `text/csv` with the header `full_name,first_name,last_name,title,email,linkedin_url,accounts,added_by,added_at`. `accounts` reads `Acme (evaluator, P2); Globex (user)`; `added_by` is Ken, User, API key or Workflow; `added_at` is the UTC date. Cells that start like a formula are neutralized. More than 50,000 rows is `400 validation_failed` with detail `too_many_rows`: narrow the filters. Each export writes an audit-log entry with the filters and the row count. People made from Kenso library list contacts are never exported; the response header `X-Ken-Excluded-Library-People` says how many the filters matched, including 0. With account scope `own`, only people on the caller's accounts, each with only those accounts. Requires `ExportAccess`.
21835
+ Body: the `people.search` filters and `sort`, no paging. Answers a streamed `text/csv` with the header `full_name,first_name,last_name,title,email,linkedin_url,accounts,added_by,added_at`. `accounts` reads `Acme (evaluator, P2); Globex (user)`; `added_by` is Ken, User, API key or Workflow; `added_at` is the UTC date. Cells that start like a formula are neutralized. More than 50,000 rows is `400 validation_failed` with detail `too_many_rows`: narrow the filters. Each export writes an audit-log entry with the filters and the row count. People made from Kenso library list contacts are never exported; the response header `X-Ken-Excluded-Library-People` says how many the filters matched, including 0. With account scope `own`, only people on the caller's accounts, each with only those accounts. Requires Accounts view and the `export` permission.
21687
21836
 
21688
21837
  - Beta: needs the `accounts` feature flag on the workspace.
21689
21838
  - API: `POST /v1/people/export`
@@ -21714,7 +21863,7 @@ kenso people export --data @body.json
21714
21863
 
21715
21864
  Queue a find for the email of each person without one.
21716
21865
 
21717
- Body: `personIds` (1-500) and `accountId` (optional: the account whose domain to search). Skips people not in the workspace (`not_found`), with an email (`has_email`) or with a find already running (`active_find`). `estimate.maxCredits` is the most this can cost, 1 credit per queued find, charged only when an email is found. Out of credits, the finds wait as paused and `paused` is true. Returns `202`; the person's `emailFind` shows progress. With account scope `own`, people on none of the caller's accounts are skipped as `not_found`. Requires `WriteAccess`.
21866
+ Body: `personIds` (1-500) and `accountId` (optional: the account whose domain to search). Skips people not in the workspace (`not_found`), with an email (`has_email`) or with a find already running (`active_find`). `estimate.maxCredits` is the most this can cost, 1 credit per queued find, charged only when an email is found. Out of credits, the finds wait as paused and `paused` is true. Returns `202`; the person's `emailFind` shows progress. With account scope `own`, people on none of the caller's accounts are skipped as `not_found`. Requires Accounts edit.
21718
21867
 
21719
21868
  - Beta: needs the `accounts` feature flag on the workspace.
21720
21869
  - API: `POST /v1/people/email-finds`
@@ -21737,7 +21886,7 @@ kenso people find-emails --data @body.json
21737
21886
 
21738
21887
  Queue a find for the phone of each person without one.
21739
21888
 
21740
- Body: `personIds` (1-500). Returns `202` with how many people were `queued`, `alreadyHavePhone` (a valid phone, nothing queued), `alreadyQueued` (a find already queued, running or paused) and `skipped` (not in the workspace), and `personIds` naming the people of each. A find is charged only when a number is found, at the provider's cost; out of credits, the finds wait as paused. The person's `phoneFind` shows progress, and a found number joins `phones`. With account scope `own`, people on none of the caller's accounts are skipped. Behind the `calls` feature (404 when off). Requires `WriteAccess`.
21889
+ Body: `personIds` (1-500). Returns `202` with how many people were `queued`, `alreadyHavePhone` (a valid phone, nothing queued), `alreadyQueued` (a find already queued, running or paused) and `skipped` (not in the workspace), and `personIds` naming the people of each. A find is charged only when a number is found, at the provider's cost; out of credits, the finds wait as paused. The person's `phoneFind` shows progress, and a found number joins `phones`. With account scope `own`, people on none of the caller's accounts are skipped. Behind the `calls` feature (404 when off). Requires Accounts edit.
21741
21890
 
21742
21891
  - Beta: needs the `accounts` and `calls` feature flag on the workspace.
21743
21892
  - API: `POST /v1/people/phone-finds`
@@ -21759,7 +21908,7 @@ kenso people find-phones --data @body.json
21759
21908
 
21760
21909
  Return one person with the accounts they are a stakeholder on.
21761
21910
 
21762
- Each account carries the person's `stakeholderId`, `role`, `priority` and `coverage` there, highest priority first. `emailFind` is the newest find of their email. `tagIds` are the tags they carry; a champion carries the Champion tag. A merged person is `404` with detail `person_merged` and `mergedIntoId`, the person to read instead. Requires `ReadAccess`.
21911
+ Each account carries the person's `stakeholderId`, `role`, `priority` and `coverage` there, highest priority first. `emailFind` is the newest find of their email. `tagIds` are the tags they carry; a champion carries the Champion tag. A merged person is `404` with detail `person_merged` and `mergedIntoId`, the person to read instead. Requires Accounts view.
21763
21912
 
21764
21913
  - Beta: needs the `accounts` feature flag on the workspace.
21765
21914
  - API: `GET /v1/people/{personId}`
@@ -21779,7 +21928,7 @@ kenso people get --person-id <person-id>
21779
21928
 
21780
21929
  Return a person's research: the latest run's status and the latest finished research.
21781
21930
 
21782
- Query (optional): `mode`, `light` or `heavy`, to read only that mode's runs (a newer Light run otherwise shows instead of the Heavy research); without it, runs of either mode. `status` is the latest run's: `none` (never researched), `queued`, `running`, `succeeded`, `no_data` (Ken found no data on the person, so nothing was written) or `failed` with `errorCode` (`person_deleted`, `source_unavailable`, `model_failed`, `invalid_output`, `timeout`). `mode` is the latest run's (`light` or `heavy`), `requestedAt` when it was opened. The rest comes from the latest succeeded run, so research stays visible while a new run is in progress or after one found nothing: `researchedMode`, `researchedAt`, `summary`, `worthKnowing` (points worth knowing before outreach, each `{text, sourceIds}`), `careerMoves` (newest first, `{text, date, companyName, title, sourceIds}`, date `YYYY`, `YYYY-MM` or `YYYY-MM-DD`) and `sources` (`{id, kind, title, url, date}`, kind `linkedin_profile`, `work_history`, `campaign_contact` or `linkedin_post`, and on Heavy `web`, `x`, `instagram`, `facebook`, `news` or `podcast`). Heavy research adds `webFindings` (`{text, kind, url, sourceIds}`, kind and url of the first source), `firecrawlCredits` and, when the web part is missing, `webNote` (for example "Web research unavailable: Firecrawl ..."). Every finding cites the ids of the sources it came from. An unknown or deleted person, another workspace's, or with account scope `own` a person on none of the caller's accounts, is `404 not_found`. A merged person is `404` with detail `person_merged` and `mergedIntoId`. Requires `ReadAccess` and the `accounts_research` feature.
21931
+ Query (optional): `mode`, `light` or `heavy`, to read only that mode's runs (a newer Light run otherwise shows instead of the Heavy research); without it, runs of either mode. `status` is the latest run's: `none` (never researched), `queued`, `running`, `succeeded`, `no_data` (Ken found no data on the person, so nothing was written) or `failed` with `errorCode` (`person_deleted`, `source_unavailable`, `model_failed`, `invalid_output`, `timeout`). `mode` is the latest run's (`light` or `heavy`), `requestedAt` when it was opened. The rest comes from the latest succeeded run, so research stays visible while a new run is in progress or after one found nothing: `researchedMode`, `researchedAt`, `summary`, `worthKnowing` (points worth knowing before outreach, each `{text, sourceIds}`), `careerMoves` (newest first, `{text, date, companyName, title, sourceIds}`, date `YYYY`, `YYYY-MM` or `YYYY-MM-DD`) and `sources` (`{id, kind, title, url, date}`, kind `linkedin_profile`, `work_history`, `campaign_contact` or `linkedin_post`, and on Heavy `web`, `x`, `instagram`, `facebook`, `news` or `podcast`). Heavy research adds `webFindings` (`{text, kind, url, sourceIds}`, kind and url of the first source), `firecrawlCredits` and, when the web part is missing, `webNote` (for example "Web research unavailable: Firecrawl ..."). Every finding cites the ids of the sources it came from. An unknown or deleted person, another workspace's, or with account scope `own` a person on none of the caller's accounts, is `404 not_found`. A merged person is `404` with detail `person_merged` and `mergedIntoId`. Requires Accounts view and the `accounts_research` feature.
21783
21932
 
21784
21933
  - Beta: needs the `accounts` and `accounts_research` feature flag on the workspace.
21785
21934
  - API: `GET /v1/people/{personId}/research`
@@ -21823,7 +21972,7 @@ kenso people import --file people.jsonl --dry-run
21823
21972
 
21824
21973
  Take in one person from your own source: match, place on their company's account, assign an owner.
21825
21974
 
21826
- Body: `email`, `linkedinUrl`, or a name (`fullName`, or `firstName` and `lastName`) with a business `companyDomain` (else `400``identity_required`); `title`, `companyName`, `phone` (a bad number is `400``invalid_phone`), `source` (`api` default, `workflow` or `manual`; any other is `400``invalid_source`), `sourceDetail`, `ownerMemberId` (a member, else `400``invalid_member`), `leadStatus` (default `new`), `customFields` (person custom field keys; unknown keys are ignored) and `createAccount`. A person with the same email, else LinkedIn URL, else the same name on the domain and no email, is matched: only their blank fields fill, pinned fields never change, the phone is added and the latest source updates. Else the person is created with lead status new and a score. A business domain with no account gets one (stage researching, source inbound, no research credits) unless `createAccount` is false or the workspace's intake settings say no; the person joins that account. Owner: the person's, else the account's, the CRM contact's, `ownerMemberId`, the workspace default, then the round robin. A new person adds a new_lead row to the owner's Today and notifies them. Returns `person`, `created`, `matchedBy` (email, linkedin_url, name_domain or none), `accountId`, `accountCreated` and `suggestedDuplicateOfPersonId` (same name on the domain with another email). A role address (support@, info@, no-reply@) matches the person who holds it but never fills onto one. With a LinkedIn URL or a real first and last name (as on `people.create`) the person comes in without it, matched later by that name on the address's domain, and the answer carries `emailDropped: role_address`; with neither and no match it is `422 role_address` and nothing is written. 600 calls an hour per workspace; the next is `429` with `Retry-After`. With account scope `own` it is `403``permission_denied` with `scope: all`. Behind the `lead_intake` feature (404 when off). Requires `WriteAccess`.
21975
+ Body: `email`, `linkedinUrl`, or a name (`fullName`, or `firstName` and `lastName`) with a business `companyDomain` (else `400``identity_required`); `title`, `companyName`, `phone` (a bad number is `400``invalid_phone`), `source` (`api` default, `workflow` or `manual`; any other is `400``invalid_source`), `sourceDetail`, `ownerMemberId` (a member, else `400``invalid_member`), `leadStatus` (default `new`), `customFields` (person custom field keys; unknown keys are ignored) and `createAccount`. A person with the same email, else LinkedIn URL, else the same name on the domain and no email, is matched: only their blank fields fill, pinned fields never change, the phone is added and the latest source updates. Else the person is created with lead status new and a score. A business domain with no account gets one (stage researching, source inbound, no research credits) unless `createAccount` is false or the workspace's intake settings say no; the person joins that account. Owner: the person's, else the account's, the CRM contact's, `ownerMemberId`, the workspace default, then the round robin. A new person adds a new_lead row to the owner's Today and notifies them. Returns `person`, `created`, `matchedBy` (email, linkedin_url, name_domain or none), `accountId`, `accountCreated` and `suggestedDuplicateOfPersonId` (same name on the domain with another email). A role address (support@, info@, no-reply@) matches the person who holds it but never fills onto one. With a LinkedIn URL or a real first and last name (as on `people.create`) the person comes in without it, matched later by that name on the address's domain, and the answer carries `emailDropped: role_address`; with neither and no match it is `422 role_address` and nothing is written. 600 calls an hour per workspace; the next is `429` with `Retry-After`. With account scope `own` it is `403``permission_denied` with `scope: all`. Behind the `lead_intake` feature (404 when off). Requires Accounts edit.
21827
21976
 
21828
21977
  - Beta: needs the `accounts` and `lead_intake` feature flag on the workspace.
21829
21978
  - API: `POST /v1/people/intake`
@@ -21862,7 +22011,7 @@ kenso people intake --data @body.json
21862
22011
 
21863
22012
  List people in the current workspace.
21864
22013
 
21865
- Filters: `email`, `linkedinUrl` (any profile URL shape; normalized), `q` (name or email contains), `ownerMemberId`, `leadStatus` (comma list: new, working, qualified, disqualified, nurture), `source` (comma list on the latest source), `tagIds` (comma list of tag ids: people carrying any of them; `GET /v1/tags?systemKey=champion` gives the Champion tag), `behaviorTags` (repeatable: people whose behavior tags include any, for example `behaviorTags=cold_responder&behaviorTags=prefers_linkedin`; needs the `behavior_signals` feature; `email_confirmed`, `email_dead_end` and `left_company` cannot be filtered). `sort`: `-createdAt` (default), `name` (A-Z, unnamed last), `-score` (highest first, unscored last), `-latestSourceAt` (newest first, none last). Each person carries `ownerMemberId`, `leadStatus`, `score`, `phones`, `phone` (the first valid one), `firstSource`, `latestSource` and `tagIds`. Requires `ReadAccess`.
22014
+ Filters: `email`, `linkedinUrl` (any profile URL shape; normalized), `q` (name or email contains), `ownerMemberId`, `leadStatus` (comma list: new, working, qualified, disqualified, nurture), `source` (comma list on the latest source), `tagIds` (comma list of tag ids: people carrying any of them; `GET /v1/tags?systemKey=champion` gives the Champion tag), `behaviorTags` (repeatable: people whose behavior tags include any, for example `behaviorTags=cold_responder&behaviorTags=prefers_linkedin`; needs the `behavior_signals` feature; `email_confirmed`, `email_dead_end` and `left_company` cannot be filtered). `sort`: `-createdAt` (default), `name` (A-Z, unnamed last), `-score` (highest first, unscored last), `-latestSourceAt` (newest first, none last). Each person carries `ownerMemberId`, `leadStatus`, `score`, `phones`, `phone` (the first valid one), `firstSource`, `latestSource` and `tagIds`. Requires Accounts view.
21866
22015
 
21867
22016
  - Beta: needs the `accounts` feature flag on the workspace.
21868
22017
  - API: `GET /v1/people` (a list: `--limit`, `--cursor`, `--all`)
@@ -21894,7 +22043,7 @@ kenso people list --all
21894
22043
 
21895
22044
  Merge this person into another person.
21896
22045
 
21897
- Body: `intoPersonId`, the person to keep. In one transaction the kept person keeps every value it has and takes this person's values for its blank fields; this person's account places move to it (on a shared account the two places combine, with touches, reports and seats re-pointed); removal history moves too; this person's active email find is cancelled. This person then reads as `404``person_merged` with `mergedIntoId`. Merging into the same, a deleted or a merged person is `400 validation_failed`. Returns the kept person. With account scope `own` it is `403``permission_denied` with `scope: all`. Requires `WriteAccess`.
22046
+ Body: `intoPersonId`, the person to keep. In one transaction the kept person keeps every value it has and takes this person's values for its blank fields; this person's account places move to it (on a shared account the two places combine, with touches, reports and seats re-pointed); removal history moves too; this person's active email find is cancelled. This person then reads as `404``person_merged` with `mergedIntoId`. Merging into the same, a deleted or a merged person is `400 validation_failed`. Returns the kept person. With account scope `own` it is `403``permission_denied` with `scope: all`. Requires Accounts edit.
21898
22047
 
21899
22048
  - Beta: needs the `accounts` feature flag on the workspace.
21900
22049
  - API: `POST /v1/people/{personId}/merge`
@@ -21918,7 +22067,7 @@ kenso people merge --person-id <person-id> --data @body.json
21918
22067
 
21919
22068
  Upload or replace a person's photo.
21920
22069
 
21921
- Multipart field `file`: a non-empty PNG, JPEG or WebP under 10 MB, checked by type, extension and content (else `400``validation_failed`). Kenso stores it and serves it from `/media`; it shows as `photoUrl` with `photoSource: upload`, ahead of the person's LinkedIn photo, and replaces an earlier upload. A merged person is `404``person_merged`. Returns the person. Requires `WriteAccess`.
22070
+ Multipart field `file`: a non-empty PNG, JPEG or WebP under 10 MB, checked by type, extension and content (else `400``validation_failed`). Kenso stores it and serves it from `/media`; it shows as `photoUrl` with `photoSource: upload`, ahead of the person's LinkedIn photo, and replaces an earlier upload. A merged person is `404``person_merged`. Returns the person. Requires Accounts edit.
21922
22071
 
21923
22072
  - Beta: needs the `accounts` feature flag on the workspace.
21924
22073
  - API: `PUT /v1/people/{personId}/photo`
@@ -21965,7 +22114,7 @@ kenso people research --person-id <person-id> --data @body.json
21965
22114
 
21966
22115
  Search people in the current workspace with filters.
21967
22116
 
21968
- Body, every field optional: `q` (full name, email or title contains), `accountIds` (on any of these accounts, up to 100), `onAccount` (`any` default, `none` on no live account, `some` on at least one), `roles` (holds one of these roles on some live account), `hasEmail`, `addedBy` (who created the person: user, agent, api_key, system, workflow), `ownerMemberId`, `leadStatus` (list), `source` (list, on the latest source), `tagIds` (carries any of these tags, up to 100), `sort` (`-createdAt` default, `name` A-Z with unnamed people last, `-score` highest first with unscored last, or `-latestSourceAt` newest first with none last), `limit` (1-200, default 50), `cursor`, `includeTotal`. Lists OR within themselves; filters AND together. Each row embeds its live accounts (with `stakeholderId`, `role`, `priority`, `coverage`), `emailFind` and `tagIds`. Deleted and merged people never appear. With account scope `own`, only people on the caller's accounts, each with only those accounts. Requires `ReadAccess`.
22117
+ Body, every field optional: `q` (full name, email or title contains), `accountIds` (on any of these accounts, up to 100), `onAccount` (`any` default, `none` on no live account, `some` on at least one), `roles` (holds one of these roles on some live account), `hasEmail`, `addedBy` (who created the person: user, agent, api_key, system, workflow), `ownerMemberId`, `leadStatus` (list), `source` (list, on the latest source), `tagIds` (carries any of these tags, up to 100), `sort` (`-createdAt` default, `name` A-Z with unnamed people last, `-score` highest first with unscored last, or `-latestSourceAt` newest first with none last), `limit` (1-200, default 50), `cursor`, `includeTotal`. Lists OR within themselves; filters AND together. Each row embeds its live accounts (with `stakeholderId`, `role`, `priority`, `coverage`), `emailFind` and `tagIds`. Deleted and merged people never appear. With account scope `own`, only people on the caller's accounts, each with only those accounts. Requires Accounts view.
21969
22118
 
21970
22119
  - Beta: needs the `accounts` feature flag on the workspace.
21971
22120
  - API: `POST /v1/people/search`
@@ -22000,7 +22149,7 @@ kenso people search --data @body.json
22000
22149
 
22001
22150
  Update person fields. Absent fields stay unchanged; null clears.
22002
22151
 
22003
- Fields: fullName, firstName, lastName, title, email, linkedinUrl, kenPersonId, `ownerMemberId` (a workspace member, else `400``invalid_member`), `leadStatus` (new, working, qualified, disqualified, nurture), `phones` (`[{ number, kind }]`, up to 20; replaces the list, so send every number to keep; a number the person has keeps when it was added; a number marked invalid (a wrong number) stays even when left out, after the numbers sent; a bad number is `400``invalid_phone`; to add or remove one number use `PATCH /v1/people/{personId}/phones`), `phoneOptOut` (true: the person asked not to be called; it records now and the caller as `phoneOptOutAt`, keeping an earlier opt-out's time; false clears it; Kenso's dialer never calls an opted-out person). `score` and the sources are read-only. An owner or status change writes `person.owner_changed` or `person.lead_status_changed` with before and after; each new number writes `person.phone_added`. An email or LinkedIn URL another person holds is `409 conflict` (`email_taken`, `linkedin_url_taken`) with that `personId`, which is left out when account scope `own` hides that person. A person keeps at least one of fullName, email, linkedinUrl. Pinned fields need `overwritePinned: true`. A merged person is `404``person_merged`. Requires `WriteAccess`.
22152
+ Fields: fullName, firstName, lastName, title, email, linkedinUrl, kenPersonId, `ownerMemberId` (a workspace member, else `400``invalid_member`), `leadStatus` (new, working, qualified, disqualified, nurture), `phones` (`[{ number, kind }]`, up to 20; replaces the list, so send every number to keep; a number the person has keeps when it was added; a number marked invalid (a wrong number) stays even when left out, after the numbers sent; a bad number is `400``invalid_phone`; to add or remove one number use `PATCH /v1/people/{personId}/phones`), `phoneOptOut` (true: the person asked not to be called; it records now and the caller as `phoneOptOutAt`, keeping an earlier opt-out's time; false clears it; Kenso's dialer never calls an opted-out person). `score` and the sources are read-only. An owner or status change writes `person.owner_changed` or `person.lead_status_changed` with before and after; each new number writes `person.phone_added`. An email or LinkedIn URL another person holds is `409 conflict` (`email_taken`, `linkedin_url_taken`) with that `personId`, which is left out when account scope `own` hides that person. A person keeps at least one of fullName, email, linkedinUrl. Pinned fields need `overwritePinned: true`. A merged person is `404``person_merged`. Requires Accounts edit.
22004
22153
 
22005
22154
  - Beta: needs the `accounts` feature flag on the workspace.
22006
22155
  - API: `PATCH /v1/people/{personId}`
@@ -22040,7 +22189,7 @@ kenso people update --person-id <person-id> --data @body.json
22040
22189
 
22041
22190
  Add, remove or reorder a person's phones without resending the list.
22042
22191
 
22043
- Body, at least one of: `add` (`[{ number, kind }]`, up to 20; a new number goes after the person's phones, recorded as added now by `manual` for a person or `api` for an API key or agent, and writes `person.phone_added`; a number the person has keeps its entry and takes the `kind` sent, and a number marked invalid (a wrong number) stays marked; `400``out_of_range` when the person would end up with more than 20 phones, wrong numbers included), `remove` (up to 20 numbers; a valid number is removed; a number marked invalid stays, so a known wrong number is never added back; a number the person does not have is ignored), `primary` (a valid number the person has after `add` and `remove`; it moves first and becomes `phone`, else `400``unknown_phone`). Applied as remove, then add, then primary, to the phones stored when the write runs, so two edits at once both land. Numbers are normalized like `people.update` phones; a bad one is `400``invalid_phone` naming it. Phones a person set need `overwritePinned: true` from an API key or agent. A merged person is `404``person_merged`. Returns the person, like `people.update`. Requires `WriteAccess`.
22192
+ Body, at least one of: `add` (`[{ number, kind }]`, up to 20; a new number goes after the person's phones, recorded as added now by `manual` for a person or `api` for an API key or agent, and writes `person.phone_added`; a number the person has keeps its entry and takes the `kind` sent, and a number marked invalid (a wrong number) stays marked; `400``out_of_range` when the person would end up with more than 20 phones, wrong numbers included), `remove` (up to 20 numbers; a valid number is removed; a number marked invalid stays, so a known wrong number is never added back; a number the person does not have is ignored), `primary` (a valid number the person has after `add` and `remove`; it moves first and becomes `phone`, else `400``unknown_phone`). Applied as remove, then add, then primary, to the phones stored when the write runs, so two edits at once both land. Numbers are normalized like `people.update` phones; a bad one is `400``invalid_phone` naming it. Phones a person set need `overwritePinned: true` from an API key or agent. A merged person is `404``person_merged`. Returns the person, like `people.update`. Requires Accounts edit.
22044
22193
 
22045
22194
  - Beta: needs the `accounts` feature flag on the workspace.
22046
22195
  - API: `PATCH /v1/people/{personId}/phones`
@@ -22359,7 +22508,7 @@ kenso permission-templates update --template-id <template-id> --data @body.json
22359
22508
 
22360
22509
  Return a person's behavior signals.
22361
22510
 
22362
- The same body as `contact-behavior.get` for the person's LinkedIn profile (else their email): `workspace` and, with the `behavior_signals_network` feature, `network` with `workspaces` (other workspaces only). Address tags (`email_confirmed`, `email_dead_end`, `left_company`) are never on a person: read them on a contact. Absent means null. An unknown or deleted person, another workspace's, or with account scope `own` a person on none of the caller's accounts, is `404 not_found`; a merged person is `404` with detail `person_merged` and `mergedIntoId`. Requires `ReadAccess` and the `accounts` and `behavior_signals` features.
22511
+ The same body as `contact-behavior.get` for the person's LinkedIn profile (else their email): `workspace` and, with the `behavior_signals_network` feature, `network` with `workspaces` (other workspaces only). Address tags (`email_confirmed`, `email_dead_end`, `left_company`) are never on a person: read them on a contact. Absent means null. An unknown or deleted person, another workspace's, or with account scope `own` a person on none of the caller's accounts, is `404 not_found`; a merged person is `404` with detail `person_merged` and `mergedIntoId`. Requires Accounts view and the `accounts` and `behavior_signals` features.
22363
22512
 
22364
22513
  - Beta: needs the `accounts` and `behavior_signals` feature flag on the workspace.
22365
22514
  - API: `GET /v1/people/{personId}/behavior`
@@ -22381,7 +22530,7 @@ kenso person-behavior get --person-id <person-id>
22381
22530
 
22382
22531
  Start an inbox placement test for a campaign.
22383
22532
 
22384
- Requires Delivery edit. Exactly one of `domainId` or `inboxId` is required unless `randomScope` is true. Example: `{ "campaignId": 812, "inboxId": 44 }`. Kenso pays for each test, so each call counts against a per-workspace budget: `placementTestCreates` (5 a minute, 20 a day, 50 in 30 days; a Free plan gets 1, 2 and 5), over it `429 rate_limited` with `Retry-After`. Where `placement_tests_public` is on, the budget is `placementTestCreatesPublic` (2 a day, 8 in 30 days, on every plan), over it `429 quota_exceeded` naming the limit, with `Retry-After`. Super admins are exempt. `openPixel: true` sends the copy with the campaign open pixel on `link.` or `unsub.` of the test's sending domain, whichever serves it; `openPixelHost` names the host used. No serving host, plain-text copy, or a test with the other pixel setting already running for the campaign and domain is `409 invalid_state`.
22533
+ Requires Delivery edit. Exactly one of `domainId` or `inboxId` is required unless `randomScope` is true. Example: `{ "campaignId": 812, "inboxId": 44 }`. Kenso pays for each test, so each call counts against a per-workspace budget: `placementTestCreates` (5 a minute, 20 a day, 50 in 30 days; a Free plan gets 1, 2 and 5), over it `429 rate_limited` with `Retry-After`. Where `placement_tests_public` is on, the budget is `placementTestCreatesPublic` (2 a day, 8 in 30 days, on every plan), over it `429 quota_exceeded` naming the limit, with `Retry-After`. Kenso staff are exempt. `openPixel: true` sends the copy with the campaign open pixel on `link.` or `unsub.` of the test's sending domain, whichever serves it; `openPixelHost` names the host used. No serving host, plain-text copy, or a test with the other pixel setting already running for the campaign and domain is `409 invalid_state`.
22385
22534
 
22386
22535
  - API: `POST /v1/placement-tests`
22387
22536
  - Generated from the API contract
@@ -22418,7 +22567,7 @@ kenso placement-tests create --data @body.json
22418
22567
 
22419
22568
  Return one placement test with its full results.
22420
22569
 
22421
- Adds what the list leaves out: `type`, `startedAt`, `sentCount`, `senderInboxIds`, the `subject` and `body` sent, `counts`, per `providers`, per `senders`, per `seeds` (masked addresses), InboxKit `errors`, and up to 10 `earlierTests` of the test's own campaign and domain (whichever are set) for a trend; a shared weekly test trends with the campaign that owns it, and an inbox test without a domain has none. Result fields are omitted until the test completes. Rates are fractions from 0 to 1. Requires `ReadAccess`. A test outside the workspace is `not_found`.
22570
+ Adds what the list leaves out: `type`, `startedAt`, `sentCount`, `senderInboxIds`, the `subject` and `body` sent, `counts`, per `providers`, per `senders`, per `seeds` (masked addresses), InboxKit `errors`, and up to 10 `earlierTests` of the test's own campaign and domain (whichever are set) for a trend; a shared weekly test trends with the campaign that owns it, and an inbox test without a domain has none. Result fields are omitted until the test completes. Rates are fractions from 0 to 1. Requires Delivery view. A test outside the workspace is `not_found`.
22422
22571
 
22423
22572
  - API: `GET /v1/placement-tests/{placementTestId}`
22424
22573
  - Generated from the API contract
@@ -22437,7 +22586,7 @@ kenso placement-tests get --placement-test-id <placement-test-id>
22437
22586
 
22438
22587
  List placement tests for a campaign, newest first.
22439
22588
 
22440
- Requires `campaignId`. Default sort is `-createdAt`. Rates are fractions from 0 to 1. Requires `ReadAccess`. A campaign outside the workspace is `not_found`.
22589
+ Requires `campaignId`. Default sort is `-createdAt`. Rates are fractions from 0 to 1. Requires Delivery view. A campaign outside the workspace is `not_found`.
22441
22590
 
22442
22591
  - API: `GET /v1/placement-tests` (a list: `--limit`, `--cursor`, `--all`)
22443
22592
  - Generated from the API contract
@@ -22480,7 +22629,7 @@ kenso portal-sessions create
22480
22629
 
22481
22630
  Create a prompt template in the current workspace.
22482
22631
 
22483
- Returns `201` and `Location`. `scope=workspace` is owned by the workspace header. Qualification prompts keep `IQualificationWorkflowSyncService` in sync. A campaign or model configuration outside the workspace is `not_found` before the write. Requires `WriteAccess`.
22632
+ Returns `201` and `Location`. `scope=workspace` is owned by the workspace header. Qualification prompts keep `IQualificationWorkflowSyncService` in sync. A campaign or model configuration outside the workspace is `not_found` before the write. Requires Campaigns edit.
22484
22633
 
22485
22634
  - API: `POST /v1/prompt-templates`
22486
22635
  - Generated from the API contract
@@ -22507,7 +22656,7 @@ kenso prompt-templates create --data @body.json
22507
22656
 
22508
22657
  Delete a prompt template in the current workspace.
22509
22658
 
22510
- Returns `204`. A template outside the workspace is `not_found` and is not deleted. Requires `WriteAccess`.
22659
+ Returns `204`. A template outside the workspace is `not_found` and is not deleted. Requires Campaigns edit.
22511
22660
 
22512
22661
  - API: `DELETE /v1/prompt-templates/{templateId}`
22513
22662
  - Generated from the API contract
@@ -22526,7 +22675,7 @@ kenso prompt-templates delete --template-id <template-id>
22526
22675
 
22527
22676
  Return one prompt template visible in the current workspace.
22528
22677
 
22529
- A missing or cross-workspace id is `not_found`. Requires `ReadAccess`.
22678
+ A missing or cross-workspace id is `not_found`. Requires Campaigns view.
22530
22679
 
22531
22680
  - API: `GET /v1/prompt-templates/{templateId}`
22532
22681
  - Generated from the API contract
@@ -22545,7 +22694,7 @@ kenso prompt-templates get --template-id <template-id>
22545
22694
 
22546
22695
  List prompt templates visible in the current workspace.
22547
22696
 
22548
- Ordered by id. Optional `flow` and `scope` narrow the page. `q` matches the name. Workspace-scoped rows are limited to `X-Ken-Workspace-Id`. Requires `ReadAccess`.
22697
+ Ordered by id. Optional `flow` and `scope` narrow the page. `q` matches the name. Workspace-scoped rows are limited to `X-Ken-Workspace-Id`. Requires Campaigns view.
22549
22698
 
22550
22699
  - API: `GET /v1/prompt-templates` (a list: `--limit`, `--cursor`, `--all`)
22551
22700
  - Generated from the API contract
@@ -22570,7 +22719,7 @@ kenso prompt-templates list --all
22570
22719
 
22571
22720
  Update a prompt template in the current workspace.
22572
22721
 
22573
- Absent fields stay unchanged. `null` clears `description` and `modelConfigurationId`. A template outside the workspace is `not_found` before the write. Qualification prompt edits keep `IQualificationWorkflowSyncService` in sync. Requires `WriteAccess`.
22722
+ Absent fields stay unchanged. `null` clears `description` and `modelConfigurationId`. A template outside the workspace is `not_found` before the write. Qualification prompt edits keep `IQualificationWorkflowSyncService` in sync. Requires Campaigns edit.
22574
22723
 
22575
22724
  - API: `PATCH /v1/prompt-templates/{templateId}`
22576
22725
  - Generated from the API contract
@@ -22594,7 +22743,7 @@ kenso prompt-templates update --template-id <template-id> --data @body.json
22594
22743
 
22595
22744
  Return one prompt version for a segment in the current workspace.
22596
22745
 
22597
- A missing segment or version is `not_found`. Requires `ReadAccess`.
22746
+ A missing segment or version is `not_found`. Requires Campaigns view.
22598
22747
 
22599
22748
  - API: `GET /v1/segments/{segmentId}/prompt-versions/{versionId}`
22600
22749
  - Generated from the API contract
@@ -22614,7 +22763,7 @@ kenso prompt-versions get --segment-id <segment-id> --version-id <version-id>
22614
22763
 
22615
22764
  List prompt versions for a segment in the current workspace.
22616
22765
 
22617
- A segment outside `X-Ken-Workspace-Id` is `not_found` before versions are read. Requires `ReadAccess`.
22766
+ A segment outside `X-Ken-Workspace-Id` is `not_found` before versions are read. Requires Campaigns view.
22618
22767
 
22619
22768
  - API: `GET /v1/segments/{segmentId}/prompt-versions` (a list: `--limit`, `--cursor`, `--all`)
22620
22769
  - Generated from the API contract
@@ -22637,7 +22786,7 @@ kenso prompt-versions list --segment-id <segment-id> --all
22637
22786
 
22638
22787
  Restore a prompt version for a segment in the current workspace.
22639
22788
 
22640
- Qualification and scoring workflow sync stay inside the existing revert command. A segment outside the workspace is `not_found` before the write. `dryRun` returns what the restore would clear and does not write. A restore that would clear campaign AI output returns `409 conflict` with `error.impact` until `confirm` is true. The body is optional. Requires `WriteAccess`.
22789
+ Qualification and scoring workflow sync stay inside the existing revert command. A segment outside the workspace is `not_found` before the write. `dryRun` returns what the restore would clear and does not write. A restore that would clear campaign AI output returns `409 conflict` with `error.impact` until `confirm` is true. The body is optional. Requires Campaigns edit.
22641
22790
 
22642
22791
  - API: `POST /v1/segments/{segmentId}/prompt-versions/{versionId}/restore`
22643
22792
  - Generated from the API contract
@@ -22666,7 +22815,7 @@ kenso prompt-versions restore --segment-id <segment-id> --version-id <version-id
22666
22815
 
22667
22816
  Create a prompt in the current workspace.
22668
22817
 
22669
- Returns `201` and `Location`. `campaignId` or `segmentId` is required so the prompt stays in the workspace. `confirm=true` is required when the write would refresh cached AI results. Requires `WriteAccess`.
22818
+ Returns `201` and `Location`. `campaignId` or `segmentId` is required so the prompt stays in the workspace. `confirm=true` is required when the write would refresh cached AI results. Requires Campaigns edit.
22670
22819
 
22671
22820
  - API: `POST /v1/prompts`
22672
22821
  - Generated from the API contract
@@ -22707,7 +22856,7 @@ kenso prompts create --confirm --data @body.json
22707
22856
 
22708
22857
  Delete a prompt.
22709
22858
 
22710
- Returns `204`. `confirm=true` is required when deletion would refresh cached AI results. Requires `WriteAccess`.
22859
+ Returns `204`. `confirm=true` is required when deletion would refresh cached AI results. Requires Campaigns edit.
22711
22860
 
22712
22861
  - API: `DELETE /v1/prompts/{promptId}`
22713
22862
  - Generated from the API contract
@@ -22728,7 +22877,7 @@ kenso prompts delete --prompt-id <prompt-id> --confirm
22728
22877
 
22729
22878
  Return one prompt in the current workspace.
22730
22879
 
22731
- A missing or cross-workspace id is `not_found`. Requires `ReadAccess`.
22880
+ A missing or cross-workspace id is `not_found`. Requires Campaigns view.
22732
22881
 
22733
22882
  - API: `GET /v1/prompts/{promptId}`
22734
22883
  - Generated from the API contract
@@ -22771,7 +22920,7 @@ kenso prompts link-knowledge-prompt --prompt-id <prompt-id> --data @body.json
22771
22920
 
22772
22921
  List prompts in the current workspace.
22773
22922
 
22774
- Ordered by id. Optional `campaignId`, `segmentId`, and `flow` narrow the page. `q` matches the name. Requires `ReadAccess`.
22923
+ Ordered by id. Optional `campaignId`, `segmentId`, and `flow` narrow the page. `q` matches the name. Requires Campaigns view.
22775
22924
 
22776
22925
  - API: `GET /v1/prompts` (a list: `--limit`, `--cursor`, `--all`)
22777
22926
  - Generated from the API contract
@@ -22797,7 +22946,7 @@ kenso prompts list --all
22797
22946
 
22798
22947
  Run a prompt test against live model calls.
22799
22948
 
22800
- Samples contacts in the campaign and runs the requested flow. This spends model usage. A missing campaign is `not_found`. Requires `PromptTestAccess`.
22949
+ Samples contacts in the campaign and runs the requested flow. This spends model usage. A missing campaign is `not_found`. Requires Campaigns edit and the `ask` permission.
22801
22950
 
22802
22951
  - API: `POST /v1/prompts/test`
22803
22952
  - Generated from the API contract
@@ -22842,7 +22991,7 @@ kenso prompts unlink-knowledge-prompt --prompt-id <prompt-id>
22842
22991
 
22843
22992
  Update a prompt.
22844
22993
 
22845
- Absent fields stay unchanged. JSON null clears a text field or `promptTemplateId`. `segmentId` cannot change on this route, and `campaignId` cannot be cleared on a prompt without a segment (`400`: it would belong to no workspace). `confirm=true` is required when the write would refresh cached AI results. Requires `WriteAccess`.
22994
+ Absent fields stay unchanged. JSON null clears a text field or `promptTemplateId`. `segmentId` cannot change on this route, and `campaignId` cannot be cleared on a prompt without a segment (`400`: it would belong to no workspace). `confirm=true` is required when the write would refresh cached AI results. Requires Campaigns edit.
22846
22995
 
22847
22996
  - API: `PATCH /v1/prompts/{promptId}`
22848
22997
  - Generated from the API contract
@@ -22907,7 +23056,7 @@ kenso provider-keys delete --provider <provider>
22907
23056
 
22908
23057
  List every provider and the workspace's key status.
22909
23058
 
22910
- Returns every catalogue provider, live and coming soon, in one list. This is a catalogue, not a cursor API: `pagination.nextCursor` is null and `pagination.hasMore` is false. Never returns a key or any prefix of one; `keySuffix` is the last four characters. AI providers also carry `inFlightAiBatchCount`, the workspace's AI batches still running on that key, and a `blitz` or `moltsets` key carries `quota`. `openai` is left out while `sign_in_with_chatgpt` is off for the caller, unless the workspace already saved an OpenAI key. That saved key can be changed, verified and deleted, but not replaced: `PUT openai` answers 409 even though the entry may read `live`. Any workspace member may call it. Requires `ReadAccess`.
23059
+ Returns every catalogue provider, live and coming soon, in one list. This is a catalogue, not a cursor API: `pagination.nextCursor` is null and `pagination.hasMore` is false. Never returns a key or any prefix of one; `keySuffix` is the last four characters. AI providers also carry `inFlightAiBatchCount`, the workspace's AI batches still running on that key, and a `blitz` or `moltsets` key carries `quota`. `openai` is left out while `sign_in_with_chatgpt` is off for the caller, unless the workspace already saved an OpenAI key. That saved key can be changed, verified and deleted, but not replaced: `PUT openai` answers 409 even though the entry may read `live`. Requires the `integrations` permission.
22911
23060
 
22912
23061
  - API: `GET /v1/provider-keys`
22913
23062
  - Generated from the API contract
@@ -23256,7 +23405,7 @@ kenso public-shares submit --token <token> --data @body.json
23256
23405
 
23257
23406
  Create a reply template.
23258
23407
 
23259
- The author is the caller: `user` for a person in the dashboard, `agent` for an agent, an MCP token or an API key. `authorSource``agent` only lowers a person to `agent`. Requires Inbox edit access and the workspace settings permission (owners, admins and editors by default).
23408
+ The author is the caller: `user` for a person in the dashboard, `agent` for an agent, an MCP token or an API key. `authorSource``agent` only lowers a person to `agent`. Requires Inbox edit and the `workspace_settings` permission (owners, admins and editors by default).
23260
23409
 
23261
23410
  - API: `POST /v1/reply-templates`
23262
23411
  - Generated from the API contract
@@ -23281,7 +23430,7 @@ kenso reply-templates create --data @body.json
23281
23430
 
23282
23431
  Delete a reply template.
23283
23432
 
23284
- Returns `204`. The author is the caller, as on create. An agent, an MCP token or an API key cannot delete a human-written template. Requires Inbox edit access and the workspace settings permission (owners, admins and editors by default).
23433
+ Returns `204`. The author is the caller, as on create. An agent, an MCP token or an API key cannot delete a human-written template. Requires Inbox edit and the `workspace_settings` permission (owners, admins and editors by default).
23285
23434
 
23286
23435
  - API: `DELETE /v1/reply-templates/{replyTemplateId}`
23287
23436
  - Generated from the API contract
@@ -23301,7 +23450,7 @@ kenso reply-templates delete --reply-template-id <reply-template-id>
23301
23450
 
23302
23451
  Return one reply template in the current workspace.
23303
23452
 
23304
- A missing or cross-workspace id is `not_found`. Requires `ReadAccess`.
23453
+ A missing or cross-workspace id is `not_found`. Requires Inbox view.
23305
23454
 
23306
23455
  - API: `GET /v1/reply-templates/{replyTemplateId}`
23307
23456
  - Generated from the API contract
@@ -23320,7 +23469,7 @@ kenso reply-templates get --reply-template-id <reply-template-id>
23320
23469
 
23321
23470
  List reply templates in the current workspace.
23322
23471
 
23323
- Ordered by id. Requires `ReadAccess`.
23472
+ Ordered by id. Requires Inbox view.
23324
23473
 
23325
23474
  - API: `GET /v1/reply-templates` (a list: `--limit`, `--cursor`, `--all`)
23326
23475
  - Generated from the API contract
@@ -23342,7 +23491,7 @@ kenso reply-templates list --all
23342
23491
 
23343
23492
  Update a reply template.
23344
23493
 
23345
- Only the fields sent are written; absent fields keep their stored value. JSON null clears `description`. The author is the caller, as on create, and a person's write records `user`. An agent, an MCP token or an API key cannot edit a human-written template. Requires Inbox edit access and the workspace settings permission (owners, admins and editors by default).
23494
+ Only the fields sent are written; absent fields keep their stored value. JSON null clears `description`. The author is the caller, as on create, and a person's write records `user`. An agent, an MCP token or an API key cannot edit a human-written template. Requires Inbox edit and the `workspace_settings` permission (owners, admins and editors by default).
23346
23495
 
23347
23496
  - API: `PATCH /v1/reply-templates/{replyTemplateId}`
23348
23497
  - Generated from the API contract
@@ -23985,7 +24134,7 @@ kenso saved-lists create-source --saved-list-id <saved-list-id> --data @body.jso
23985
24134
 
23986
24135
  Add a CSV's rows to a saved list.
23987
24136
 
23988
- Multipart: the CSV in field `file` (at most 400 MB and 100,000 rows) and, optionally, `columnMappings`: the JSON array the contact-import upload takes, for example `[{"csvColumn":"Email","targetField":"email"},{"csvColumn":"Deal size","targetField":"custom_variable","customVariableName":"deal_size"}]`. Without it the columns map by their headers, as on the campaign upload. Optional List check fields, behind the `csv_import_check` feature (a field sent while it is off is `400 validation_failed` on that field): `fixes`, a JSON array of the fixes left on in a `POST /v1/contact-imports/scans` with target `saved_list` (`blocklist`, `invalidEmail`, `noMx`, `catchAll`, `personalEmail`, `roleAddress`; `missingKey` is accepted and changes nothing), for example `["blocklist","invalidEmail","noMx"]`: the rows the check flags for them are skipped; and `cleanup` (`true` runs every cell of a field with its own cleaner through the import cleaner first). A row already on the list, or a repeat of a row that was kept, still merges into its member with all its addresses: the fixes skip rows, they never strip one address from a merging row. Kenso checks the file and answers 202 with a `csv` source in `pending`, then adds the rows in the background: read the source until it is `done` or `failed`; its `parameters` then carry `rowCount`, `inserted`, `merged` and `rejected` (rows with no LinkedIn URL, email or name and company domain; for a company list, no domain or company page; and the rows a chosen fix skipped, which `skips` counts by fix key), plus the `fixes` and `cleanup` sent. Uploaded emails enter unverified, every phone is kept, and personal columns (birth date, gender, age, personal email and the like) are never stored. Only the workspace's own lists: a shared or library list is 404. Requires Search edit and the Import feature.
24137
+ Multipart: the CSV in field `file` (at most 400 MB and 100,000 rows) and, optionally, `columnMappings`: the JSON array the contact-import upload takes, for example `[{"csvColumn":"Email","targetField":"email"},{"csvColumn":"Deal size","targetField":"custom_variable","customVariableName":"deal_size"}]`. Without it the columns map by their headers, as on the campaign upload. Optional List check fields, behind the `csv_import_check` feature (a field sent while it is off is `400 validation_failed` on that field): `fixes`, a JSON array of the fixes left on in a `POST /v1/contact-imports/scans` with target `saved_list` (`blocklist`, `invalidEmail`, `noMx`, `catchAll`, `personalEmail`, `roleAddress`; `missingKey` is accepted and changes nothing), for example `["blocklist","invalidEmail","noMx"]`: the rows the check flags for them are skipped; and `cleanup` (`true` runs every cell of a field with its own cleaner through the import cleaner first). A row already on the list, or a repeat of a row that was kept, still merges into its member with all its addresses: the fixes skip rows, they never strip one address from a merging row. Kenso checks the file and answers 202 with a `csv` source in `pending`, then adds the rows in the background: read the source until it is `done` or `failed`; its `parameters` then carry `rowCount`, `inserted`, `merged` and `rejected` (rows with no LinkedIn URL, email or name and company domain; for a company list, no domain or company page; and the rows a chosen fix skipped, which `skips` counts by fix key), plus the `fixes` and `cleanup` sent. Uploaded emails enter unverified, every phone is kept, and personal columns (birth date, gender, age, personal email and the like) are never stored. Only the workspace's own lists: a shared or library list is 404. Requires Search edit and the `import` permission.
23989
24138
 
23990
24139
  - Beta: needs the `saved_lists` feature flag on the workspace.
23991
24140
  - API: `POST /v1/saved-lists/{savedListId}/uploads`
@@ -24416,7 +24565,7 @@ kenso saved-searches update --saved-search-id <saved-search-id>
24416
24565
 
24417
24566
  Cancel a scheduled message that has not started sending.
24418
24567
 
24419
- Requires `InboxReplyAccess`. A missing id is `not_found`. A row that is already sending, sent, cancelled, or failed is `invalid_state`. Cancel still returns the scheduled message when restoring the composer draft fails, with `draft.saved` false so the caller saves the draft again.
24568
+ Requires Inbox edit and the `reply` permission. A missing id is `not_found`. A row that is already sending, sent, cancelled, or failed is `invalid_state`. Cancel still returns the scheduled message when restoring the composer draft fails, with `draft.saved` false so the caller saves the draft again.
24420
24569
 
24421
24570
  - API: `POST /v1/scheduled-messages/{scheduledMessageId}/cancel`
24422
24571
  - Generated from the API contract
@@ -24437,7 +24586,7 @@ kenso scheduled-messages cancel --scheduled-message-id <scheduled-message-id>
24437
24586
 
24438
24587
  Start a batch scrape and return a job.
24439
24588
 
24440
- Requires `ReadAccess`. The response is `202` with a `batch_scrape` job. Poll `GET /v1/jobs/{jobId}?type=batch_scrape`. Cancel with `POST /v1/jobs/{jobId}/cancel?type=batch_scrape`.
24589
+ Requires Search edit. The response is `202` with a `batch_scrape` job. Poll `GET /v1/jobs/{jobId}?type=batch_scrape`. Cancel with `POST /v1/jobs/{jobId}/cancel?type=batch_scrape`.
24441
24590
 
24442
24591
  - API: `POST /v1/scraping/batch-scrapes`
24443
24592
  - Generated from the API contract
@@ -24458,7 +24607,7 @@ kenso scraping create-batch-scrape --data @body.json
24458
24607
 
24459
24608
  Start a crawl and return a job.
24460
24609
 
24461
- Requires `ReadAccess`. The response is `202` with a `crawl` job. Poll `GET /v1/jobs/{jobId}?type=crawl`. Cancel with `POST /v1/jobs/{jobId}/cancel?type=crawl`. SSRF rejection stays in the Firecrawl service. The body needs a `limit` of 1 to 1,000 pages. When the job cannot be recorded, the started crawl is stopped and the answer is `503 service_unavailable`.
24610
+ Requires Search edit. The response is `202` with a `crawl` job. Poll `GET /v1/jobs/{jobId}?type=crawl`. Cancel with `POST /v1/jobs/{jobId}/cancel?type=crawl`. SSRF rejection stays in the Firecrawl service. The body needs a `limit` of 1 to 1,000 pages. When the job cannot be recorded, the started crawl is stopped and the answer is `503 service_unavailable`.
24462
24611
 
24463
24612
  - API: `POST /v1/scraping/crawls`
24464
24613
  - Generated from the API contract
@@ -24479,7 +24628,7 @@ kenso scraping create-crawl --data @body.json
24479
24628
 
24480
24629
  Start an extraction and return a job.
24481
24630
 
24482
- Requires `ReadAccess`. The response is `202` with an `extraction` job. Poll `GET /v1/jobs/{jobId}?type=extraction`. Extraction cannot be canceled.
24631
+ Requires Search edit. The response is `202` with an `extraction` job. Poll `GET /v1/jobs/{jobId}?type=extraction`. Extraction cannot be canceled.
24483
24632
 
24484
24633
  - API: `POST /v1/scraping/extractions`
24485
24634
  - Generated from the API contract
@@ -25199,7 +25348,7 @@ kenso segment-email-sequences get-version --segment-id <segment-id> --version-id
25199
25348
 
25200
25349
  List saved versions of a segment email sequence, newest first.
25201
25350
 
25202
- `limit` is 1 to 100 and defaults to 20. Requires `ReadAccess`.
25351
+ `limit` is 1 to 100 and defaults to 20. Requires Campaigns view.
25203
25352
 
25204
25353
  - API: `GET /v1/segments/{segmentId}/email-sequence/versions` (a list: `--limit`, `--cursor`, `--all`)
25205
25354
  - Generated from the API contract
@@ -25272,7 +25421,7 @@ kenso segment-email-sequences replace --segment-id <segment-id> --data @body.jso
25272
25421
 
25273
25422
  Restore a segment email sequence to a saved version.
25274
25423
 
25275
- A link the URL reputation check refuses is 400 `validation_failed` with one `link_reputation_blocked` detail per link; its `field` is `steps.subject` or `steps.body` and `stepId` is the live step the restore would overwrite. Requires `SequenceEditAccess`.
25424
+ A link the URL reputation check refuses is 400 `validation_failed` with one `link_reputation_blocked` detail per link; its `field` is `steps.subject` or `steps.body` and `stepId` is the live step the restore would overwrite. Requires Campaigns edit.
25276
25425
 
25277
25426
  - API: `POST /v1/segments/{segmentId}/email-sequence/versions/{versionId}/restore`
25278
25427
  - Generated from the API contract
@@ -25292,7 +25441,7 @@ kenso segment-email-sequences restore-version --segment-id <segment-id> --versio
25292
25441
 
25293
25442
  Save a new segment email sequence version.
25294
25443
 
25295
- Omit `steps` to snapshot the live sequence. With `sourceVersionId`, omitted or empty `steps` keep that version's steps, and a non-empty list overlays them. A link the URL reputation check refuses is 400 `validation_failed` with one `link_reputation_blocked` detail per link (`field`, `stepId`, `stepOrder`, `rule`, `url`), as on the sequence replace. Requires `SequenceEditAccess`.
25444
+ Omit `steps` to snapshot the live sequence. With `sourceVersionId`, omitted or empty `steps` keep that version's steps, and a non-empty list overlays them. A link the URL reputation check refuses is 400 `validation_failed` with one `link_reputation_blocked` detail per link (`field`, `stepId`, `stepOrder`, `rule`, `url`), as on the sequence replace. Requires Campaigns edit.
25296
25445
 
25297
25446
  - API: `POST /v1/segments/{segmentId}/email-sequence/versions`
25298
25447
  - Generated from the API contract
@@ -25467,7 +25616,7 @@ kenso segment-linkedin-sequences save-version --segment-id <segment-id> --data @
25467
25616
 
25468
25617
  Activate a segment.
25469
25618
 
25470
- Idempotent when the segment is already active. Requires `WriteAccess`.
25619
+ Idempotent when the segment is already active. Requires Campaigns edit.
25471
25620
 
25472
25621
  - API: `POST /v1/segments/{segmentId}/activate`
25473
25622
  - Generated from the API contract
@@ -25486,7 +25635,7 @@ kenso segments activate --segment-id <segment-id>
25486
25635
 
25487
25636
  Create a segment on a campaign in the current workspace.
25488
25637
 
25489
- Returns `201` and `Location`. Requires `WriteAccess`.
25638
+ Returns `201` and `Location`. Requires Campaigns edit.
25490
25639
 
25491
25640
  - API: `POST /v1/campaigns/{campaignId}/segments`
25492
25641
  - Generated from the API contract
@@ -25513,7 +25662,7 @@ kenso segments create --campaign-id <campaign-id> --data @body.json
25513
25662
 
25514
25663
  Deactivate a segment.
25515
25664
 
25516
- Idempotent when the segment is already inactive. Requires `WriteAccess`.
25665
+ Idempotent when the segment is already inactive. Requires Campaigns edit.
25517
25666
 
25518
25667
  - API: `POST /v1/segments/{segmentId}/deactivate`
25519
25668
  - Generated from the API contract
@@ -25551,7 +25700,7 @@ kenso segments delete --segment-id <segment-id>
25551
25700
 
25552
25701
  Return one segment in the current workspace.
25553
25702
 
25554
- A missing or cross-workspace id is `not_found`. Requires `ReadAccess`.
25703
+ A missing or cross-workspace id is `not_found`. Requires Campaigns view.
25555
25704
 
25556
25705
  - API: `GET /v1/segments/{segmentId}`
25557
25706
  - Generated from the API contract
@@ -25570,7 +25719,7 @@ kenso segments get --segment-id <segment-id>
25570
25719
 
25571
25720
  Return the segment filter catalog.
25572
25721
 
25573
- The values a segment filter accepts: seniority levels, functions, industries, company types, employment types, funding rounds, technologies, email validity, email providers and languages, each as `value`, `label` and an optional `category`. The catalog is the same for every workspace and is the `filterOptions` object `POST /v1/campaigns/{campaignId}/segments/preview` returns; this read needs no campaign. It replaces `GET /v1/campaigns/segments/filter-metadata`. Read-only. Requires `ReadAccess`.
25722
+ The values a segment filter accepts: seniority levels, functions, industries, company types, employment types, funding rounds, technologies, email validity, email providers and languages, each as `value`, `label` and an optional `category`. The catalog is the same for every workspace and is the `filterOptions` object `POST /v1/campaigns/{campaignId}/segments/preview` returns; this read needs no campaign. It replaces `GET /v1/campaigns/segments/filter-metadata`. Read-only. Requires Campaigns view.
25574
25723
 
25575
25724
  - API: `GET /v1/segments/filter-options`
25576
25725
  - Generated from the API contract
@@ -25588,7 +25737,7 @@ kenso segments get-filter-options
25588
25737
 
25589
25738
  Return one segment version and its snapshot.
25590
25739
 
25591
- `segmentationType` and `status` on the snapshot are lowercase snake_case. Requires `ReadAccess`.
25740
+ `segmentationType` and `status` on the snapshot are lowercase snake_case. Requires Campaigns view.
25592
25741
 
25593
25742
  - API: `GET /v1/segments/{segmentId}/versions/{versionId}`
25594
25743
  - Generated from the API contract
@@ -25608,7 +25757,7 @@ kenso segments get-version --segment-id <segment-id> --version-id <version-id>
25608
25757
 
25609
25758
  List segments of a campaign in the current workspace.
25610
25759
 
25611
- Ordered by id. `q` matches the name. `isActive` filters that flag. `includeMetrics=false` skips the contact counts and omits `contactCount`, `rawContactCount` and `readyContactCount`: use it when only ids and names are needed. Read-only. Requires `ReadAccess`.
25760
+ Ordered by id. `q` matches the name. `isActive` filters that flag. `includeMetrics=false` skips the contact counts and omits `contactCount`, `rawContactCount` and `readyContactCount`: use it when only ids and names are needed. Read-only. Requires Campaigns view.
25612
25761
 
25613
25762
  - API: `GET /v1/campaigns/{campaignId}/segments` (a list: `--limit`, `--cursor`, `--all`)
25614
25763
  - Generated from the API contract
@@ -25636,7 +25785,7 @@ kenso segments list --campaign-id <campaign-id> --all
25636
25785
 
25637
25786
  List saved versions of a segment, newest first.
25638
25787
 
25639
- `limit` is 1 to 100 and defaults to 20. `cursor` is the opaque cursor from the previous page. Requires `ReadAccess`.
25788
+ `limit` is 1 to 100 and defaults to 20. `cursor` is the opaque cursor from the previous page. Requires Campaigns view.
25640
25789
 
25641
25790
  - API: `GET /v1/segments/{segmentId}/versions` (a list: `--limit`, `--cursor`, `--all`)
25642
25791
  - Generated from the API contract
@@ -25659,7 +25808,7 @@ kenso segments list-versions --segment-id <segment-id> --all
25659
25808
 
25660
25809
  Preview a segment filter against the campaign.
25661
25810
 
25662
- Returns the match count, a contact sample, and the filter catalog (`filterOptions`). Omit the body to read the catalog only, or read it without a campaign at `GET /v1/segments/filter-options`. A body, including an empty object, runs the match preview. `segmentId` scopes a match preview to an existing segment. Read-only. Requires `ReadAccess`.
25811
+ Returns the match count, a contact sample, and the filter catalog (`filterOptions`). Omit the body to read the catalog only, or read it without a campaign at `GET /v1/segments/filter-options`. A body, including an empty object, runs the match preview. A body whose keys name no filter field is `400 validation_failed` naming those keys, as on create and update. `segmentId` scopes a match preview to an existing segment. Read-only. Requires Campaigns view.
25663
25812
 
25664
25813
  - API: `POST /v1/campaigns/{campaignId}/segments/preview`
25665
25814
  - Generated from the API contract
@@ -25706,7 +25855,7 @@ kenso segments preview --campaign-id <campaign-id> --data @body.json
25706
25855
 
25707
25856
  Restore a segment to a saved version.
25708
25857
 
25709
- `dryRun` returns the cascade preview and does not write. Each child includes the resolved `snapshot` the revert dialog renders. An applying restore that would clear campaign AI output returns `409 conflict` with `error.impact` until `confirm` is true. Cascade `kind` and `status` are lowercase snake_case. Requires `WriteAccess`.
25858
+ `dryRun` returns the cascade preview and does not write. Each child includes the resolved `snapshot` the revert dialog renders. An applying restore that would clear campaign AI output returns `409 conflict` with `error.impact` until `confirm` is true. Cascade `kind` and `status` are lowercase snake_case. Requires Campaigns edit.
25710
25859
 
25711
25860
  - API: `POST /v1/segments/{segmentId}/versions/{versionId}/restore`
25712
25861
  - Generated from the API contract
@@ -25733,7 +25882,7 @@ kenso segments restore-version --segment-id <segment-id> --version-id <version-i
25733
25882
 
25734
25883
  Save a segment as a new version.
25735
25884
 
25736
- Applies the body to the segment and writes one version row. The body takes the `PATCH /v1/segments/{segmentId}` fields plus `sourceVersionId`. Absent fields keep the live segment's value, or the `sourceVersionId` version's value when one is given, so a save with no body saves the current state. JSON null clears `description` and `abTestPercentage`. A `sourceVersionId` of another segment is `not_found`, and another save in progress for the segment is `409 conflict`. When the save lands but version history is not captured, `version` is null and `versionWarning` says so. Requires `WriteAccess`.
25885
+ Applies the body to the segment and writes one version row. The body takes the `PATCH /v1/segments/{segmentId}` fields plus `sourceVersionId`. Absent fields keep the live segment's value, or the `sourceVersionId` version's value when one is given, so a save with no body saves the current state. JSON null clears `description` and `abTestPercentage`. A `sourceVersionId` of another segment is `not_found`, and another save in progress for the segment is `409 conflict`. When the save lands but version history is not captured, `version` is null and `versionWarning` says so. Requires Campaigns edit.
25737
25886
 
25738
25887
  - API: `POST /v1/segments/{segmentId}/versions`
25739
25888
  - Generated from the API contract
@@ -25761,7 +25910,7 @@ kenso segments save-version --segment-id <segment-id> --data @body.json
25761
25910
 
25762
25911
  Update a segment.
25763
25912
 
25764
- Absent fields stay unchanged. JSON null clears `description` and `abTestPercentage`. `filterCriteria` field names are camelCase (matched case-insensitively); an object that names no filter field is `400 validation_failed` and nothing is saved. Requires `WriteAccess`.
25913
+ Absent fields stay unchanged. JSON null clears `description` and `abTestPercentage`. `filterCriteria` field names are camelCase (matched case-insensitively); an object that names no filter field is `400 validation_failed` and nothing is saved. Requires Campaigns edit.
25765
25914
 
25766
25915
  - API: `PATCH /v1/segments/{segmentId}`
25767
25916
  - Generated from the API contract
@@ -25828,7 +25977,7 @@ kenso sending-capacity get
25828
25977
 
25829
25978
  ### kenso sending-capacity get-forecast
25830
25979
 
25831
- Forecast the current workspace's email sends for UTC today and the next two days: what its healthy inboxes can send per day (capacity), what is already queued or planned (planned), and what is left (remaining). Public, read only, Delivery view. Takes no what-if inputs; the staff forecast keeps those.
25980
+ Forecast the current workspace's email sends for UTC today and the next two days: what its healthy inboxes can send per day (capacity), what is already queued or planned (planned), and what is left (remaining). Read-only. Requires Delivery view. Takes no what-if inputs.
25832
25981
 
25833
25982
  - API: `GET /v1/sending-capacity/forecast`
25834
25983
  - Generated from the API contract
@@ -26037,7 +26186,7 @@ kenso sending-providers list
26037
26186
 
26038
26187
  Return the sequence variable catalog.
26039
26188
 
26040
- Pass `campaignId` to include that campaign's custom variables. Requires `ReadAccess`.
26189
+ Pass `campaignId` to include that campaign's custom variables. Requires Campaigns view.
26041
26190
 
26042
26191
  - API: `GET /v1/sequence-variables`
26043
26192
  - Generated from the API contract
@@ -26058,7 +26207,7 @@ kenso sequence-variables get
26058
26207
 
26059
26208
  Read the meeting notes shared with you.
26060
26209
 
26061
- The token is the last path segment of the emailed link `/meeting-share/{token}`. No `X-Ken-Workspace-Id` needed. Returns the meeting's title and times, who shared it, when the link expires and Ken's notes (`notes` is null until there are notes to read); never a rep's typed lines or the transcript. The signed-in email must be the shared address (`403 email_mismatch`) and verified, by the session's `email_verified` claim or the user's stored verified email (`403 email_unverified`). An unknown or altered token, or a deleted meeting, is `404`. A revoked share is `410 share_revoked`, an expired one `410 share_expired`, and Kenso Notetaker off for the sharer in that workspace `410 share_inactive`. An agent or an API key gets `403`: only a person signed in with the address opens it. Each open is audited.
26210
+ The token is the last path segment of the emailed link `/meeting-share/{token}`. No `X-Ken-Workspace-Id` needed. Returns the meeting's title and times, who shared it, when the link expires and Ken's notes (`notes` is null until there are notes to read); never a rep's typed lines or the transcript. An unknown or altered token is `404`. Then the address is checked before anything else about the share: the signed-in email must be the shared address (`403 email_mismatch`, whatever state the share is in) and verified, by the session's `email_verified` claim or the user's stored verified email (`403 email_unverified`). Only then the share's state: a deleted meeting is `404`, a revoked share is `410 share_revoked`, an expired one `410 share_expired`, and Kenso Notetaker off for the sharer in that workspace `410 share_inactive`. So a `410` or `200` means the caller holds the shared address. An agent or an API key gets `403`: only a person signed in with the address opens it. Each open is audited, once per person and share per 2 minutes.
26062
26211
 
26063
26212
  - API: `GET /v1/shared-meetings/{token}`
26064
26213
  - Generated from the API contract
@@ -26341,7 +26490,7 @@ kenso signal-vendor-connections get --integration-key <integration-key>
26341
26490
 
26342
26491
  Queue signals for the current workspace.
26343
26492
 
26344
- Requires Search edit access (owners, admins and editors by default). `type` and `source` are lowercase snake_case. Non-super-admins are forced to synthetic signals and send at most 100 signals a call, each counted against the workspace's signal quota. A contact, campaign, or segment outside the workspace is rejected before publish. `duplicateCount` counts signals skipped because their `dedupKey` was queued in the last 24 hours.
26493
+ Requires Search edit access (owners, admins and editors by default). `type` and `source` are lowercase snake_case. Every signal is stored as synthetic. A call sends at most 100 signals, each counted against the workspace's signal quota. A contact, campaign, or segment outside the workspace is rejected before publish. `duplicateCount` counts signals skipped because their `dedupKey` was queued in the last 24 hours.
26345
26494
 
26346
26495
  - API: `POST /v1/signals`
26347
26496
  - Generated from the API contract
@@ -26566,7 +26715,7 @@ kenso spend-limits update --data @body.json
26566
26715
 
26567
26716
  Add a person to an account as a stakeholder.
26568
26717
 
26569
- Send `personId` (an existing person) or `person` (found by email, then LinkedIn URL, then kenPersonId, else created; a match only fills its blank fields), plus `role` (champion, economic_buyer, approver, evaluator, user, executive_sponsor, blocker, unknown), `priority` (1-3), `coverage` (engaged, planned, gap, not_targeted), `note`, `kenNote` (up to 1,000 characters), `reportsToStakeholderId` (a stakeholder on the same account, no cycles) and `fillsSeatId` (an open seat on this account, closed in the same write). Conflicts: `already_stakeholder`; `previously_removed` when a person removed them and the caller is not a person (resend with `force: true` only when the user asked). Requires `WriteAccess`.
26718
+ Send `personId` (an existing person) or `person` (found by email, then LinkedIn URL, then kenPersonId, else created; a match only fills its blank fields), plus `role` (champion, economic_buyer, approver, evaluator, user, executive_sponsor, blocker, unknown), `priority` (1-3), `coverage` (engaged, planned, gap, not_targeted), `note`, `kenNote` (up to 1,000 characters), `reportsToStakeholderId` (a stakeholder on the same account, no cycles) and `fillsSeatId` (an open seat on this account, closed in the same write). Conflicts: `already_stakeholder`; `previously_removed` when a person removed them and the caller is not a person (resend with `force: true` only when the user asked). Requires Accounts edit.
26570
26719
 
26571
26720
  - Beta: needs the `accounts` feature flag on the workspace.
26572
26721
  - API: `POST /v1/accounts/{accountId}/stakeholders`
@@ -26600,7 +26749,7 @@ kenso stakeholders create --account-id <account-id> --data @body.json
26600
26749
 
26601
26750
  Remove a stakeholder from its account.
26602
26751
 
26603
- Skips their open touches; the person stays. Returns `204`. Requires `WriteAccess`.
26752
+ Skips their open touches; the person stays. Returns `204`. Requires Accounts edit.
26604
26753
 
26605
26754
  - Beta: needs the `accounts` feature flag on the workspace.
26606
26755
  - API: `DELETE /v1/stakeholders/{stakeholderId}`
@@ -26620,7 +26769,7 @@ kenso stakeholders delete --stakeholder-id <stakeholder-id>
26620
26769
 
26621
26770
  List an account's stakeholders, each with its person.
26622
26771
 
26623
- Oldest first. Requires `ReadAccess`.
26772
+ Oldest first. Requires Accounts view.
26624
26773
 
26625
26774
  - Beta: needs the `accounts` feature flag on the workspace.
26626
26775
  - API: `GET /v1/accounts/{accountId}/stakeholders` (a list: `--limit`, `--cursor`, `--all`)
@@ -26644,7 +26793,7 @@ kenso stakeholders list --account-id <account-id> --all
26644
26793
 
26645
26794
  Update a stakeholder. Absent fields stay unchanged; null clears.
26646
26795
 
26647
- Fields: role, priority, coverage, note, kenNote, reportsToStakeholderId. Pinned fields need `overwritePinned: true`. Requires `WriteAccess`.
26796
+ Fields: role, priority, coverage, note, kenNote, reportsToStakeholderId. Pinned fields need `overwritePinned: true`. Requires Accounts edit.
26648
26797
 
26649
26798
  - Beta: needs the `accounts` feature flag on the workspace.
26650
26799
  - API: `PATCH /v1/stakeholders/{stakeholderId}`
@@ -26967,7 +27116,7 @@ kenso subscription update --data @body.json
26967
27116
 
26968
27117
  Stream the stored attachment bytes for the calling user.
26969
27118
 
26970
- Replaces `GET /v1/support-chat/attachments/{id}/content`. Requires `ReadAccess`.
27119
+ Replaces `GET /v1/support-chat/attachments/{id}/content`. Requires the `ask` permission.
26971
27120
 
26972
27121
  - API: `GET /v1/support/attachments/{attachmentId}/content`
26973
27122
  - Generated from the API contract
@@ -26986,7 +27135,7 @@ kenso support-attachments content --attachment-id <attachment-id>
26986
27135
 
26987
27136
  Upload a file for the support agent to read: PDF, Word, Markdown, text, CSV or image.
26988
27137
 
26989
- Replaces `POST /v1/support-chat/attachments`. Multipart field `file`, at most 20 MB. Takes .pdf, .docx, .md, .markdown, .txt, .csv, .png, .jpg, .jpeg, .webp and .gif, checked against the file's bytes; `kind` is `pdf`, `docx`, `text` or `image`. Each upload draws on per-user and per-workspace daily quotas; a refusal is `429`. A PDF without a text layer comes back with `textStatus``pending` while OCR reads it in the background, under the workspace's OCR page budget. Requires `ReadAccess`.
27138
+ Replaces `POST /v1/support-chat/attachments`. Multipart field `file`, at most 20 MB. Takes .pdf, .docx, .md, .markdown, .txt, .csv, .png, .jpg, .jpeg, .webp and .gif, checked against the file's bytes; `kind` is `pdf`, `docx`, `text` or `image`. Each upload draws on per-user and per-workspace daily quotas; a refusal is `429`. A PDF without a text layer comes back with `textStatus``pending` while OCR reads it in the background, under the workspace's OCR page budget. Requires the `ask` permission.
26990
27139
 
26991
27140
  - API: `POST /v1/support/attachments`
26992
27141
  - Generated from the API contract
@@ -27007,7 +27156,7 @@ kenso support-attachments create --file ./file.csv
27007
27156
 
27008
27157
  Read attachment metadata and extracted text for the calling user.
27009
27158
 
27010
- Replaces `GET /v1/support-chat/attachments/{id}`. Requires `ReadAccess`.
27159
+ Replaces `GET /v1/support-chat/attachments/{id}`. Requires the `ask` permission.
27011
27160
 
27012
27161
  - API: `GET /v1/support/attachments/{attachmentId}`
27013
27162
  - Generated from the API contract
@@ -27076,7 +27225,7 @@ kenso tags bulk-assign --tag-id <tag-id> --data @body.json
27076
27225
 
27077
27226
  Remove a tag from up to 1,000 entities in one call.
27078
27227
 
27079
- Same body, validation, and per-item behavior as `bulk-create`, through the same path as `DELETE /v1/tags/{tagId}/assignments`. A successful item is `removed`. The route needs workspace membership; each item's entity type adds its page edit access, as on assign.
27228
+ Same body, validation, and per-item behavior as `bulk-create`, through the same path as `DELETE /v1/tags/{tagId}/assignments`. A successful item is `removed`. A deleted tag of the workspace can still be removed from `reply` and `linkedin_conversation` items; every other item is then `not_found`. The route needs workspace membership; each item's entity type adds its page edit access, as on assign.
27080
27229
 
27081
27230
  - API: `POST /v1/tags/{tagId}/assignments/bulk-delete`
27082
27231
  - Generated from the API contract
@@ -27187,7 +27336,7 @@ kenso tags list --all
27187
27336
 
27188
27337
  Remove a tag assignment.
27189
27338
 
27190
- Query parameters, not a body: `entityType` plus `entityId`, `messageId`, or `conversationId` (the same ids as on assign). A missing `entityType` is `validation_failed`, and a tag outside the workspace is `404 not_found`. The route needs workspace membership; the entity type adds its page edit access, as on assign.
27339
+ Query parameters, not a body: `entityType` plus `entityId`, `messageId`, or `conversationId` (the same ids as on assign). A missing `entityType` is `validation_failed`, and a tag outside the workspace is `404 not_found`. A deleted tag of the workspace can still be removed from a `reply` or a `linkedin_conversation`. The route needs workspace membership; the entity type adds its page edit access, as on assign.
27191
27340
 
27192
27341
  - API: `DELETE /v1/tags/{tagId}/assignments`
27193
27342
  - Generated from the API contract
@@ -27343,7 +27492,7 @@ kenso teams-connection update --data @body.json
27343
27492
 
27344
27493
  Apply the same field update to up to 1,000 threads.
27345
27494
 
27346
- Returns per-id outcomes. Requires `InboxReplyAccess`.
27495
+ Returns per-id outcomes. Requires Inbox edit.
27347
27496
 
27348
27497
  - API: `POST /v1/threads/bulk-update`
27349
27498
  - Generated from the API contract
@@ -27364,7 +27513,7 @@ kenso threads bulk-update --data @body.json
27364
27513
 
27365
27514
  Count inbox threads by tab and source.
27366
27515
 
27367
- Requires Inbox view. Email and LinkedIn together. `status`, `folder`, `isArchived` and `isSaved` name the tab the way `GET /v1/threads` takes them, and `allCount` is the number of distinct threads that list returns for the tab. The list's `pagination.total` counts email messages, so it is higher when a thread has several messages in the tab. `accountsCount` and `campaignsCount` split the tab by account-linked and campaign threads, and `campaigns` lists every campaign with a thread in the inbox. `toReplyCount`, `unreadCount`, `latestMessageAt` and `inboxChangedAt` ignore the tab filters. `inboxChangedAt` moves when a reply arrives (a late-synced one included) or changes tab, for example when the classifier files it; poll it and refetch the list when it changes. An email thread is counted once however many messages it has; a LinkedIn conversation is one thread.
27516
+ Requires Inbox view. Email and LinkedIn together. `status`, `folder`, `isArchived` and `isSaved` name the tab the way `GET /v1/threads` takes them, and `allCount` is the number of distinct threads that list returns for the tab. The list's `pagination.total` counts email messages, so it is higher when a thread has several messages in the tab. `accountsCount` and `campaignsCount` split the tab by account-linked and campaign threads, and `campaigns` lists every campaign with a thread in the inbox. `toReplyCount`, `unreadCount`, `latestMessageAt` and `inboxChangedAt` ignore the tab filters. `inboxChangedAt` moves when a reply arrives (a late-synced one included) or changes tab, for example when the classifier files it; poll it and refetch the list when it changes. An email thread is counted once however many messages it has; a LinkedIn conversation is one thread. With `groupBy=person` the counts count distinct people instead (`allCount`, `accountsCount`, `campaignsCount`, `toReplyCount`, `unreadCount`, `campaigns[].threadCount`), each the number of rows `GET /v1/threads?groupBy=person` lists for that tab. A person counts under a campaign or an account by the campaign or account of their thread in the tab. A tab that lists our own sends (`folder=sent` or `folder=all`) keeps its thread counts in `allCount`, `accountsCount`, `campaignsCount` and `campaigns[].threadCount` under `groupBy=person`, since its grouped list's total may be capped; when `pagination.isTotalCapped` is true, show that total as N+. `toReplyCount` and `unreadCount` still count people (DEV-7255).
27368
27517
 
27369
27518
  - API: `GET /v1/threads/counts`
27370
27519
  - Generated from the API contract
@@ -27372,6 +27521,7 @@ Requires Inbox view. Email and LinkedIn together. `status`, `folder`, `isArchive
27372
27521
  | Flag | Type | Required | Description |
27373
27522
  | --- | --- | --- | --- |
27374
27523
  | `--folder` | string | no | Inbox folder: inbox, sent, or all. Omitted means inbound. |
27524
+ | `--group-by` | string | no | `person`: every count counts distinct people (a row of `GET /v1/threads?groupBy=person`) instead of threads (DEV-7255). Omitted counts threads. Any other value is 400. |
27375
27525
  | `--is-archived` | boolean | no | isArchived (query, boolean) |
27376
27526
  | `--is-saved` | boolean | no | isSaved (query, boolean) |
27377
27527
  | `--status` | string | no | Inbox tab token: to_reply, other, automated_reply, not_interested, human_reply. |
@@ -27413,7 +27563,7 @@ kenso threads create --data @body.json
27413
27563
 
27414
27564
  Send a message on a thread.
27415
27565
 
27416
- Email requires `senderEmailId`. An email reply goes to the thread's sender unless it names `to`, or `replyAll: true` (the sender, To and Cc, less the sending inbox); `cc` and `bcc` add recipients, each one bare email address. `attachmentIds` are `att_` ids uploaded against this thread (its newest message), and `contentType` is `html` (default) or `text`. Those six fields are email only: a LinkedIn reply that names any of them is `400 validation_failed` on that field and sends nothing (DEV-2537). LinkedIn sends plain text through the conversation seat. A LinkedIn `Idempotency-Key` is bound to its thread and message: reusing it on another thread, or with another body once decided, returns `409 idempotency_key_reused` and sends nothing. Without a key, the same text repeated in a thread more than 5 minutes later is sent again. An email `Idempotency-Key` (optional, at most 200 characters, unique in the workspace) is bound to its thread, inbox and body, and to its `to`, `cc`, `bcc`, `replyAll`, `contentType` and `attachmentIds` when it names any: a retry returns the reply it scheduled, or the `202` with the client approval item it was held as, and queues nothing; another request under it is `409 idempotency_key_reused`; one still being queued is `409 in_progress` with `nextAllowedAt` and `Retry-After`; one Kenso cannot tell was queued is `409 outcome_unknown` and never queues again. A refused reply frees its key (DEV-3672). Requires `InboxReplyAccess`. The send is recorded only after the provider accepts it. An email reply to a do-not-contact, blocklisted or unsubscribed contact returns `409 invalid_state` with a `confirmSuppressed` detail whose code is the reason, and is sent only when the request repeats with `confirmSuppressed: true`. LinkedIn replies to a suppressed contact are always refused. An email reply is recorded with its caller: a person, an API key, or an agent (any credential sent with `X-Ken-Actor: agent`). An API key or agent reply from its own member's mailbox is not sent: it returns `202` with the scheduled message in `status: awaiting_approval` and `approverMemberId`, and waits in that member's `GET /v1/approvals` until they approve it in person (DEV-6060); with `accounts` off, or from another member's own mailbox, it is `403 forbidden`. Its reply from an inbox a member paused or Ken blocked from sending is a `409 invalid_state` whose detail code is `inbox_paused` or `inbox_send_blocked`, with a `fix` (DEV-3672). A block carries `nextAllowedAt` (its end) and `Retry-After`; a pause has no known end and carries neither. A pause or block that starts during the undo window refuses the queued reply at send time: the scheduled message ends `failed`. When the workspace requires external approval for replies, an email reply is not sent: the response is `202` with `status``held_for_client_approval` and the `clientApprovalItemId` that holds it (DEV-1526). An email reply a teammate writes in person from a client seat's own inbox, with `accounts_send` on, is not sent either: it returns `202` with the scheduled message (`ScheduledMessageModel`) in `status: awaiting_approval` and `approverMemberId`, and waits in that client's `GET /v1/approvals` (DEV-2043). Email `archiveAfterSend: true` archives the thread once the reply sends; a waiting reply archives only when the approved reply sends, never while it waits. An API key or agent LinkedIn reply through a seat another member connected is not sent (DEV-6464): it returns `202` with `status: awaiting_approval`, `channel: linkedin`, the `approvalId` (type `reply.linkedin`) and `approverMemberId` (the seat owner), and waits in that member's `GET /v1/approvals` until they approve it in person, when it sends once. A retry with the same `Idempotency-Key` (or the same text without one) returns the same waiting reply. With `accounts` off for the owner, or a seat no member connected, it is `403 forbidden`. A person's own reply from the dashboard, and the seat owner's own agent or API key, send as before.
27566
+ Email requires `senderEmailId`. An email reply goes to the thread's sender unless it names `to`, or `replyAll: true` (the sender, To and Cc, less the sending inbox); `cc` and `bcc` add recipients, each one bare email address. `attachmentIds` are `att_` ids uploaded against this thread (its newest message), and `contentType` is `html` (default) or `text`. Those six fields are email only: a LinkedIn reply that names any of them is `400 validation_failed` on that field and sends nothing (DEV-2537). An email reply is queued for an undo window: `delaySeconds` is 60 (the default) or 300, as on a forward, and the `201` carries the queued reply's `scheduledMessageId`, which `POST /v1/scheduled-messages/{id}/cancel` takes until the window ends. A retry under the same `Idempotency-Key` returns the first reply whatever its delay (DEV-7673). `subject` is for drafts: a reply goes out on the thread's subject. LinkedIn sends plain text through the conversation seat at once, and ignores `delaySeconds`. A LinkedIn `Idempotency-Key` is bound to its thread and message: reusing it on another thread, or with another body once decided, returns `409 idempotency_key_reused` and sends nothing. Without a key, the same text repeated in a thread more than 5 minutes later is sent again. An email `Idempotency-Key` (optional, at most 200 characters, unique in the workspace) is bound to its thread, inbox and body, and to its `to`, `cc`, `bcc`, `replyAll`, `contentType` and `attachmentIds` when it names any: a retry returns the reply it scheduled, or the `202` with the client approval item it was held as, and queues nothing; another request under it is `409 idempotency_key_reused`; one still being queued is `409 in_progress` with `nextAllowedAt` and `Retry-After`; one Kenso cannot tell was queued is `409 outcome_unknown` and never queues again. A refused reply frees its key (DEV-3672). Requires Inbox edit and the `reply` permission. The send is recorded only after the provider accepts it. An email reply to a do-not-contact, blocklisted or unsubscribed contact returns `409 invalid_state` with a `confirmSuppressed` detail whose code is the reason, and is sent only when the request repeats with `confirmSuppressed: true`. LinkedIn replies to a suppressed contact are always refused. An email reply is recorded with its caller: a person, an API key, or an agent (any credential sent with `X-Ken-Actor: agent`). An API key or agent reply from its own member's mailbox is not sent: it returns `202` with the scheduled message in `status: awaiting_approval` and `approverMemberId`, and waits in that member's `GET /v1/approvals` until they approve it in person (DEV-6060); with `accounts` off, or from another member's own mailbox, it is `403 forbidden`. Its reply from an inbox a member paused or Ken blocked from sending is a `409 invalid_state` whose detail code is `inbox_paused` or `inbox_send_blocked`, with a `fix` (DEV-3672). A block carries `nextAllowedAt` (its end) and `Retry-After`; a pause has no known end and carries neither. A pause or block that starts during the undo window refuses the queued reply at send time: the scheduled message ends `failed`. When the workspace requires external approval for replies, an email reply is not sent: the response is `202` with `status``held_for_client_approval` and the `clientApprovalItemId` that holds it (DEV-1526). An email reply a teammate writes in person from a client seat's own inbox, with `accounts_send` on, is not sent either: it returns `202` with the scheduled message (`ScheduledMessageModel`) in `status: awaiting_approval` and `approverMemberId`, and waits in that client's `GET /v1/approvals` (DEV-2043). Email `archiveAfterSend: true` archives the thread once the reply sends; a waiting reply archives only when the approved reply sends, never while it waits. An API key or agent LinkedIn reply through a seat another member connected is not sent (DEV-6464): it returns `202` with `status: awaiting_approval`, `channel: linkedin`, the `approvalId` (type `reply.linkedin`) and `approverMemberId` (the seat owner), and waits in that member's `GET /v1/approvals` until they approve it in person, when it sends once. A retry with the same `Idempotency-Key` (or the same text without one) returns the same waiting reply. With `accounts` off for the owner, or a seat no member connected, it is `403 forbidden`. A person's own reply from the dashboard, and the seat owner's own agent or API key, send as before.
27417
27567
 
27418
27568
  - API: `POST /v1/threads/{threadId}/messages`
27419
27569
  - Generated from the API contract
@@ -27423,8 +27573,9 @@ Email requires `senderEmailId`. An email reply goes to the thread's sender unles
27423
27573
  | --- | --- | --- | --- |
27424
27574
  | `--thread-id` | string | yes | threadId (path, string) |
27425
27575
  | `--body` | string | no | body (body, string) |
27426
- | `--subject` | string | no | subject (body, string) |
27576
+ | `--subject` | string | no | Drafts only: a reply goes out on the thread's own subject, so a send ignores this field. Set a draft's subject with `PUT /v1/threads/{threadId}/draft` (DEV-7673). |
27427
27577
  | `--sender-email-id` | integer | no | senderEmailId (body, integer) |
27578
+ | `--delay-seconds` | integer | no | Email only: the undo window in seconds before the reply sends, 60 or 300, as on a forward. Omitted, it is 60. An agent sends 300. Cancel the queued reply with `POST /v1/scheduled-messages/{id}/cancel` and the response's `scheduledMessageId`. A LinkedIn reply sends at once and ignores it (DEV-7673). |
27428
27579
  | `--confirm-suppressed` | boolean | no | Email only: send even though the contact is do-not-contact, blocklisted or unsubscribed. Set only after the user confirmed a `409 invalid_state` for a suppressed contact. |
27429
27580
  | `--archive-after-send` | boolean | no | Email only: archive the thread once the reply sends. A reply waiting on a client seat's approval archives when the approved reply sends, never while it waits, and a skipped one leaves the thread as it was (DEV-2043). |
27430
27581
  | `--reply-all` | boolean | no | Email only: without Ken.Scraper.API.Dtos.CreateThreadMessageRequest.To, reply to the thread's sender, To and Cc, less the sending inbox. A LinkedIn thread refuses it (DEV-2537). |
@@ -27462,7 +27613,7 @@ kenso threads delete-draft --thread-id <thread-id>
27462
27613
 
27463
27614
  Forward an email thread to new recipients.
27464
27615
 
27465
- Requires `MemberReadAccess` (a white-label Client replies but does not forward). Email only. The forward is queued and the provider confirms delivery later. `to` is required unless `replyAll` is true. `delaySeconds` is 60 or 300. `attachmentIds` are `att_` ids for this thread. Every recipient is one bare email address. A forward that reaches the thread (`replyAll` without `to`, or the contact or another participant as a recipient) returns `409 invalid_state` when the contact is suppressed (detail code is the reason; there is no confirm override, send a reply instead) or when the workspace holds replies for external approval (detail code `client_approval_required`). A queued forward returns `200` with the scheduled message. An API key or agent forward from its own member's mailbox is not sent: it returns `202` with the scheduled message in `status: awaiting_approval` and `approverMemberId`, and waits in that member's `GET /v1/approvals` until they approve it in person (DEV-6060); with `accounts` off, or from another member's own mailbox, it is `403 forbidden`.
27616
+ Requires Inbox edit and the `reply` and `export` permissions (the built-in Client template has no `export`, so a Client seat replies but does not forward). Email only. The forward is queued and the provider confirms delivery later. `to` is required unless `replyAll` is true. `delaySeconds` is 60 or 300. `attachmentIds` are `att_` ids for this thread. Every recipient is one bare email address. A forward that reaches the thread (`replyAll` without `to`, or the contact or another participant as a recipient) returns `409 invalid_state` when the contact is suppressed (detail code is the reason; there is no confirm override, send a reply instead) or when the workspace holds replies for external approval (detail code `client_approval_required`). A queued forward returns `200` with the scheduled message. An API key or agent forward from its own member's mailbox is not sent: it returns `202` with the scheduled message in `status: awaiting_approval` and `approverMemberId`, and waits in that member's `GET /v1/approvals` until they approve it in person (DEV-6060); with `accounts` off, or from another member's own mailbox, it is `403 forbidden`.
27466
27617
 
27467
27618
  - API: `POST /v1/threads/{threadId}/forward`
27468
27619
  - Generated from the API contract
@@ -27497,7 +27648,7 @@ kenso threads forward --thread-id <thread-id> --data @body.json
27497
27648
 
27498
27649
  Return one thread.
27499
27650
 
27500
- A malformed id, unknown channel, or cross-workspace thread is `not_found`. `accountId` and `accountName` are the account whose touch the thread is, else the account on which the thread's contact is a stakeholder, when there is one. An email thread that is an account touch's thread also carries `replyTouchId`: that account's open reply touch answering the thread, which sends the reply when approved (DEV-2537). `hasDraft` says a reply draft is saved on the thread and `draftAuthor` who wrote it. Requires `ReadAccess`. An email thread carries `scheduledMessages`: replies and forwards queued on it that have not sent yet, including a teammate's reply waiting on a client seat (`awaiting_approval`, DEV-2043), which the writer can withdraw with `POST /v1/scheduled-messages/{id}/cancel`.
27651
+ A malformed id, unknown channel, or cross-workspace thread is `not_found`. `accountId` and `accountName` are the account whose touch the thread is, else the account on which the thread's contact is a stakeholder, when there is one. An email thread that is an account touch's thread also carries `replyTouchId`: that account's open reply touch answering the thread, which sends the reply when approved (DEV-2537). `hasDraft` says a reply draft is saved on the thread and `draftAuthor` who wrote it. Requires Inbox view. An email thread carries `scheduledMessages`: replies and forwards queued on it that have not sent yet, including a teammate's reply waiting on a client seat (`awaiting_approval`, DEV-2043), which the writer can withdraw with `POST /v1/scheduled-messages/{id}/cancel`.
27501
27652
 
27502
27653
  - API: `GET /v1/threads/{threadId}`
27503
27654
  - Generated from the API contract
@@ -27535,7 +27686,7 @@ kenso threads get-draft --thread-id <thread-id>
27535
27686
 
27536
27687
  List threads in the current workspace.
27537
27688
 
27538
- Requires `ReadAccess`. `channel` is `email`, `linkedin`, or `all`. `limit` defaults to 50 and maxes at 100, matching the inbox list. `status` and `folder` are the inbox tab filters (to_reply, other, automated_reply, not_interested, sent) and are applied before paging. Each thread is one row, its newest message in the tab, and `pagination.total` counts threads, as `GET /v1/threads/counts` does (DEV-4083). With `q`, `pagination.total` counts at most 1,000 threads: past 1,000 it is 1,000 and `pagination.isTotalCapped` is true, and paging goes on while `hasMore` is true. Without `q`, or at 1,000 matches or fewer, the total is exact and `isTotalCapped` is omitted (DEV-4517). To Reply is attributed Interested, Objection, and Wrong Contact, plus due reminders. A thread leaves it once our reply is its newest message, as a LinkedIn conversation does, and comes back with the lead's next reply. `source` is the lane on top of the tab: `accounts` (account touch threads) or `campaigns` (campaign threads), with the predicates of `accountsCount` and `campaignsCount` on `GET /v1/threads/counts`. Omitted lists every thread of the tab. A row of an account touch's thread carries `accountId` and `accountName` while the accounts feature is on for the caller, and an email one `replyTouchId`, the account's open reply touch answering the thread, when there is one (DEV-2537). Every row carries `hasDraft`, and `draftAuthor` (`user` or `agent`) when a reply draft is saved on it. `sort=oldest_waiting` lists the thread that has waited longest first, across pages: by `waitingSince`, else by the newest message; omitted keeps the inbox order, newest first. Each row whose newest message is the contact's carries `waitingSince`, when that message arrived, on any tab; it is a wait on a reply only on `to_reply`. A thread we answered whose reminder came due waits since that reminder (DEV-5202).
27689
+ Requires Inbox view. `channel` is `email`, `linkedin`, or `all`. `limit` defaults to 50 and maxes at 100, matching the inbox list. `status` and `folder` are the inbox tab filters (to_reply, other, automated_reply, not_interested, sent) and are applied before paging. `contactId`, `inboxId`, `hasReminder` and `interest` are applied before paging too, to each thread's row, so `hasMore` and `pagination.total` count the matching threads (DEV-7640). `inboxId` is the row's `inboxId`: a LinkedIn thread's account, or the workspace inbox an email row's message was sent to (an address in its `to`) or from (DEV-7673). `interest=not_interested` matches both not-interested classifications: the one this API writes, which reads back as `negative`, and the older one, which reads back as `not_interested`. Each thread is one row, its newest message in the tab, and `pagination.total` counts threads, as `GET /v1/threads/counts` does (DEV-4083). Under `folder=all` that is the newest message in either direction, and a message we sent carries no label and no reminder, so filter `interest` and `hasReminder` with `folder=inbox` to match each thread's newest reply. Email inbox ids and LinkedIn account ids are separate id spaces: on `channel=all` an `inboxId` matches either, so pass `channel` with it to stay on one (DEV-7693). With `q`, `pagination.total` counts at most 1,000 threads: past 1,000 it is 1,000 and `pagination.isTotalCapped` is true, and paging goes on while `hasMore` is true. Without `q`, or at 1,000 matches or fewer, the total is exact and `isTotalCapped` is omitted (DEV-4517). To Reply is attributed Interested, Objection, and Wrong Contact, plus due reminders. A thread leaves it once our reply is its newest message, as a LinkedIn conversation does, and comes back with the lead's next reply. `source` is the lane on top of the tab: `accounts` (account touch threads) or `campaigns` (campaign threads), with the predicates of `accountsCount` and `campaignsCount` on `GET /v1/threads/counts`. Omitted lists every thread of the tab. A row of an account touch's thread carries `accountId` and `accountName` while the accounts feature is on for the caller, and an email one `replyTouchId`, the account's open reply touch answering the thread, when there is one (DEV-2537). Every row carries `hasDraft`, and `draftAuthor` (`user` or `agent`) when a reply draft is saved on it. `sort=oldest_waiting` lists the thread that has waited longest first, across pages: by `waitingSince`, else by the newest message; omitted keeps the inbox order, newest first. Each row whose newest message is the contact's carries `waitingSince`, when that message arrived, on any tab; it is a wait on a reply only on `to_reply`. A thread we answered whose reminder came due waits since that reminder (DEV-5202).
27539
27690
 
27540
27691
  - API: `GET /v1/threads` (a list: `--limit`, `--cursor`, `--all`)
27541
27692
  - Generated from the API contract
@@ -27544,16 +27695,18 @@ Requires `ReadAccess`. `channel` is `email`, `linkedin`, or `all`. `limit` defau
27544
27695
  | --- | --- | --- | --- |
27545
27696
  | `--campaign-id` | integer | no | campaignId (query, integer) |
27546
27697
  | `--channel` | string | no | channel (query, string) |
27547
- | `--contact-id` | integer | no | contactId (query, integer) |
27698
+ | `--contact-id` | integer | no | Threads with this `contactId`, as each row carries it: the stored contact, or for a LinkedIn conversation stored with none, the contact its attendee is matched to (DEV-7673). |
27548
27699
  | `--cursor` | string | no | Start at this cursor (the next: hint of a previous page). |
27549
27700
  | `--folder` | string | no | Inbox folder: inbox, sent, or all. Omitted means inbound, matching the legacy list. |
27550
- | `--has-reminder` | boolean | no | hasReminder (query, boolean) |
27551
- | `--inbox-id` | integer | no | inboxId (query, integer) |
27552
- | `--interest` | string | no | interest (query, string) |
27701
+ | `--group-by` | string | no | `person`: one row per person, the person's newest thread in the tab (DEV-7255). Each row adds `personKey` (`per_`, else `con_`, else the row's `thr_` id), `personId` and `personName` (null unless `per_`), and over the person's threads on any tab `channels` (`{ email, linkedin }`), `waitingCount` (threads To Reply lists) and `threadIds` (newest first, at most 200). `pagination.total` then counts people. On a tab that lists our own sends (`folder=sent` or `folder=all`) in the default order and without `q`, the total may be capped, counting only the people among the newest messages read for the page; when `pagination.isTotalCapped` is true, show the total as N+ and page while `hasMore` is true. With `q` the total is capped at 1000 people as a plain search's is at 1000 threads. With `sort=oldest_waiting` a person sorts by their longest wait in the tab, and the row stays their newest thread. Omitted lists one row per thread. Any other value is 400. |
27702
+ | `--has-reminder` | boolean | no | true lists threads whose row carries a reminder, false those whose row carries none. Under `folder=all` a thread's row is its newest message in either direction, and a message we sent carries no reminder, so a thread we answered after setting a reminder on the reply does not match true. To filter threads by their newest reply, list with `folder=inbox` (DEV-7693). |
27703
+ | `--inbox-id` | integer | no | Threads whose `inboxId` is this inbox: a LinkedIn thread's account, or the workspace inbox an email row's message was sent to or from (DEV-7673). Email inbox ids and LinkedIn account ids are separate id spaces, so on `channel=all` (the default) the id matches an email inbox or a LinkedIn account with that id; pass `channel` to stay on one (DEV-7693). |
27704
+ | `--interest` | string | no | Threads whose row carries this label: interested, not_interested, negative, automated, unclassified, unsubscribe, objection, wrong_contact, out_of_office or referral; not_interested also matches negative. Under `folder=all` a thread's row is its newest message in either direction, and a message we sent carries no label (it reads as unclassified), so a thread we already answered matches only unclassified. To filter threads by their newest reply, list with `folder=inbox` (DEV-7693). |
27553
27705
  | `--is-archived` | boolean | no | isArchived (query, boolean) |
27554
27706
  | `--is-read` | boolean | no | isRead (query, boolean) |
27555
27707
  | `--is-saved` | boolean | no | isSaved (query, boolean) |
27556
27708
  | `--limit` | integer | no | Page size. |
27709
+ | `--person-key` | string | no | A row's `personKey` (`per_`, `con_` or `thr_`): every thread of that person, archived or not, newest first, each with the `groupBy=person` fields (DEV-7255). Every folder unless a `folder` or `status` is given; other filters still narrow it. Malformed is 400. |
27557
27710
  | `--q` | string | no | q (query, string) |
27558
27711
  | `--sort` | string | no | `oldest_waiting`: the thread that has waited longest first, across pages, by each row's `waitingSince`, else its newest message (DEV-5202). Omitted keeps the inbox order, newest first. Any other value is 400. |
27559
27712
  | `--source` | string | no | The inbox lane on top of the tab: accounts or campaigns, matching accountsCount and campaignsCount of GET /v1/threads/counts. Omitted means every thread of the tab. Any other value is 400. |
@@ -27576,12 +27729,16 @@ kenso threads list --all
27576
27729
 
27577
27730
  List messages in a thread, oldest first.
27578
27731
 
27732
+ Every item carries `kind`: `message` for a chat or email message. `includeEvents=true` on a LinkedIn thread also lists the connection note Ken sent with the invitation (`connection_note`, outbound) and the moment the person accepted (`connection_accepted`, inbound, empty body) in time order, an event before a message sent at the same instant. An email thread ignores it, and a failed read of the events returns the messages alone.
27733
+
27579
27734
  - API: `GET /v1/threads/{threadId}/messages`
27580
27735
  - Generated from the API contract
27581
27736
 
27582
27737
  | Flag | Type | Required | Description |
27583
27738
  | --- | --- | --- | --- |
27584
27739
  | `--thread-id` | string | yes | threadId (path, string) |
27740
+ | `--include-events` | boolean | no | includeEvents (query, boolean) |
27741
+ | `--no-include-events` | boolean | no | Send includeEvents as false. |
27585
27742
  | `--each` | string | no | Run once per row of a JSONL or CSV file (- for stdin); row keys fill params and body by name. |
27586
27743
  | `--concurrency` | integer | no | With --each: requests in flight at once (1-16, default 4). |
27587
27744
 
@@ -27686,7 +27843,7 @@ kenso threads suggest --thread-id <thread-id> --data @body.json
27686
27843
 
27687
27844
  Update thread fields. Absent fields stay unchanged. Null `reminderAt` clears a reminder.
27688
27845
 
27689
- Requires `InboxReplyAccess`, matching inbox triage. The thread comes back with `hasDraft` and `draftAuthor`, as threads.get serves them; `accountId`, `replyTouchId` and `scheduledMessages` are left out (DEV-2537).
27846
+ Requires Inbox edit. The thread comes back with `hasDraft` and `draftAuthor`, as threads.get serves them; `accountId`, `replyTouchId` and `scheduledMessages` are left out (DEV-2537). `personId` (a positive integer) links the thread's contact to that person, replacing any link; a merged person links as its survivor. It needs the `unified_inbox` feature for the workspace (off is `404 not_found`) and Accounts edit (without it, `403 permission_denied`). No contact is `422 unprocessable` and a person not in the workspace (or deleted) is `404 not_found`. Each of these answers before any field is written; the link is written after the other fields, so a refused field leaves no link (DEV-7255). Bulk update refuses `personId`.
27690
27847
 
27691
27848
  - API: `PATCH /v1/threads/{threadId}`
27692
27849
  - Generated from the API contract
@@ -28137,7 +28294,7 @@ kenso touches update --touch-id <touch-id> --data @body.json
28137
28294
 
28138
28295
  Create a tracking link on a campaign in the current workspace.
28139
28296
 
28140
- Returns `201` and `Location`. Requires `WriteAccess`.
28297
+ Returns `201` and `Location`. Requires Delivery edit.
28141
28298
 
28142
28299
  - API: `POST /v1/campaigns/{campaignId}/tracking-links`
28143
28300
  - Generated from the API contract
@@ -28181,7 +28338,7 @@ kenso tracking-links delete --tracking-link-id <tracking-link-id>
28181
28338
 
28182
28339
  Return one tracking link in the current workspace.
28183
28340
 
28184
- A missing or cross-workspace id is `not_found`. Requires `ReadAccess`.
28341
+ A missing or cross-workspace id is `not_found`. Requires Delivery view.
28185
28342
 
28186
28343
  - API: `GET /v1/tracking-links/{trackingLinkId}`
28187
28344
  - Generated from the API contract
@@ -28200,7 +28357,7 @@ kenso tracking-links get --tracking-link-id <tracking-link-id>
28200
28357
 
28201
28358
  List a campaign's tracking links in the current workspace.
28202
28359
 
28203
- Ordered by id. Read-only. Requires `ReadAccess`.
28360
+ Ordered by id. Read-only. Requires Delivery view.
28204
28361
 
28205
28362
  - API: `GET /v1/campaigns/{campaignId}/tracking-links` (a list: `--limit`, `--cursor`, `--all`)
28206
28363
  - Generated from the API contract
@@ -28223,7 +28380,7 @@ kenso tracking-links list --campaign-id <campaign-id> --all
28223
28380
 
28224
28381
  Update a tracking link.
28225
28382
 
28226
- Absent fields stay unchanged. JSON null clears `textValue`. Requires `WriteAccess`. A new `domainName` goes through the URL reputation check, and changing it after mail with the link went out counts against a daily cap (`429 rate_limited` with `Retry-After` once `UrlReputation:Mode` is `Enforce`). A new `domainName` on a link served from a Kenso shared tracking host is `409 shared_host_link_frozen`, and off a paid plan `402 plan_limit_reached` (`refusal.code``link_edit_requires_paid_plan`), when `Redirects:LinkEdits:Mode` is Enforce (DEV-3690).
28383
+ Absent fields stay unchanged. JSON null clears `textValue`. Requires Delivery edit. A new `domainName` goes through the URL reputation check, and changing it after mail with the link went out counts against a daily cap (`429 rate_limited` with `Retry-After` while Kenso enforces the URL reputation check). A new `domainName` on a link served from a Kenso shared tracking host is `409 shared_host_link_frozen`, and off a paid plan `402 plan_limit_reached` (`refusal.code``link_edit_requires_paid_plan`), while Kenso enforces its link edit rules (DEV-3690).
28227
28384
 
28228
28385
  - API: `PATCH /v1/tracking-links/{trackingLinkId}`
28229
28386
  - Generated from the API contract
@@ -28564,7 +28721,7 @@ kenso triggers update --trigger-id <trigger-id> --data @body.json
28564
28721
 
28565
28722
  Reserve one support-agent turn against the per-user and per-workspace monthly quotas.
28566
28723
 
28567
- Replaces `POST /v1/support-chat/turns`. A quota refusal is `429` and includes the legacy `quotaResetAt`. Requires `ReadAccess`.
28724
+ Replaces `POST /v1/support-chat/turns`. A quota refusal is `429` and includes the legacy `quotaResetAt`. Requires the `ask` permission.
28568
28725
 
28569
28726
  - API: `POST /v1/support/turns`
28570
28727
  - Generated from the API contract
@@ -28883,7 +29040,7 @@ kenso video-provider-connections verify --connection-id <connection-id>
28883
29040
 
28884
29041
  Upgrade to a WebSocket and relay PCM16 audio to Deepgram Nova 3.
28885
29042
 
28886
- Replaces `GET /v1/support-chat/voice`. Browser sockets cannot set headers, so they pass a `ticket` from `POST /v1/support/voice/tickets`. Until the browser moves to tickets, `access_token`, `ken_version=1.1`, and `workspace_id` still work. Requires `ReadAccess`.
29043
+ Replaces `GET /v1/support-chat/voice`. Browser sockets cannot set headers, so they pass a `ticket` from `POST /v1/support/voice/tickets`. Until the browser moves to tickets, `access_token`, `ken_version=1.1`, and `workspace_id` still work. Requires the `ask` permission.
28887
29044
 
28888
29045
  - API: `GET /v1/support/voice`
28889
29046
  - Generated from the API contract
@@ -28951,7 +29108,7 @@ kenso voice-notes audio --voice-note-id <voice-note-id>
28951
29108
 
28952
29109
  Mint a single-use ticket that opens the voice WebSocket.
28953
29110
 
28954
- A browser socket cannot send headers, so it opens `GET /v1/support/voice?ticket=` instead of putting the dashboard token in the URL. The ticket carries this request's `Authorization`, workspace and version, opens one socket and expires after 60 seconds. No MCP tool: only a browser socket uses it. Requires `ReadAccess`.
29111
+ A browser socket cannot send headers, so it opens `GET /v1/support/voice?ticket=` instead of putting the dashboard token in the URL. The ticket carries this request's `Authorization`, workspace and version, opens one socket and expires after 60 seconds. No MCP tool: only a browser socket uses it. Requires the `ask` permission.
28955
29112
 
28956
29113
  - API: `POST /v1/support/voice/tickets`
28957
29114
  - Generated from the API contract
@@ -31380,7 +31537,7 @@ kenso workspaces clear-reply-to --workspace-id <workspace-id>
31380
31537
 
31381
31538
  Create a workspace for the caller.
31382
31539
 
31383
- Replaces `POST /v1/workspaces/setup`. Idempotent: an existing workspace is returned with 200. A new workspace is 201. Requires `ReadAccess` because first-workspace setup is not limited to existing admins. An API key with the staff-issued scope `workspaces:provision` (a service key, DEV-1555) gets a new workspace on every call instead, with Free billing and the Kenso team as admins, and the key's owner becomes its admin. Not idempotent for such a key. The key reaches only the workspaces it is a member of.
31540
+ Replaces `POST /v1/workspaces/setup`. Idempotent: an existing workspace is returned with 200. A new workspace is 201. Open to any signed-in caller because first-workspace setup is not limited to existing admins. An API key with the staff-issued scope `workspaces:provision` (a service key, DEV-1555) gets a new workspace on every call instead, with Free billing and the Kenso team as admins, and the key's owner becomes its admin. Not idempotent for such a key. The key reaches only the workspaces it is a member of. A caller who holds no workspace and signed in with a personal, relay, agent or disposable address gets 403 `work_email_required` (DEV-7602), unless the caller is staff, signed in with LinkedIn or AgentID, or through an Auth0 organization (an accepted invitation). The rule does not apply to an API key.
31384
31541
 
31385
31542
  - API: `POST /v1/workspaces`
31386
31543
  - Generated from the API contract
@@ -31480,7 +31637,7 @@ kenso workspaces delete-wordmark --workspace-id <workspace-id>
31480
31637
 
31481
31638
  Return one workspace.
31482
31639
 
31483
- Replaces `GET /v1/clients/{id}` and `GET /v1/workspaces/{id}/company`. Non-members receive 403. Requires `ReadAccess`.
31640
+ Replaces `GET /v1/clients/{id}` and `GET /v1/workspaces/{id}/company`. Non-members receive 403. Open to any workspace member.
31484
31641
 
31485
31642
  - API: `GET /v1/workspaces/{workspaceId}`
31486
31643
  - Generated from the API contract
@@ -31516,7 +31673,7 @@ kenso workspaces get-tracking --workspace-id <workspace-id>
31516
31673
 
31517
31674
  List the caller's workspaces.
31518
31675
 
31519
- Replaces `GET /v1/workspaces/me`, `GET /v1/workspaces/search`, and `GET /v1/clients` for the caller's own memberships. Ordered by id. `q` matches the workspace name. Requires `ReadAccess`.
31676
+ Replaces `GET /v1/workspaces/me`, `GET /v1/workspaces/search`, and `GET /v1/clients` for the caller's own memberships. Ordered by id. `q` matches the workspace name. Open to any signed-in caller.
31520
31677
 
31521
31678
  - API: `GET /v1/workspaces` (a list: `--limit`, `--cursor`, `--all`)
31522
31679
  - Generated from the API contract