Kenso

Contact imports

Start a contact import and follow it as a job.

Contacts and lists3 endpointsKen-Version: 2026-11-02AuthenticationWorkspacesErrors

Preview the buyers Ken finds at a list of companies, for a campaign's ICP

POST/v1/contact-imports/buyer-previewscontact-imports.buyer-preview

Needs the csv_buyer_preview feature on your workspace. Until it is on, calls answer 404.

Try it on developer.ken.so

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-importssource: 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.

Parameters

Name
In
Type
Required
Description
Ken-Version
In: header
string, one of: 2026-11-02, 1.1
required
The API version: a dated version (YYYY-MM-DD), or 1.1, another name for the first dated version. Without it the request uses your API key's pinned version; an unpinned key reaches the legacy shape of the same path. 1.0 selects the legacy shape explicitly.
X-Ken-Workspace-Id
In: header
integer (int64)
optional
Workspace to act in. Required when your credential can reach more than one workspace (otherwise 400 workspace_required).

Request body

application/json, optional.

Field
Type
Required
Description
campaignId
integer (int64)
optional
The campaign whose ICP the buyers are matched to. Required; another workspace's campaign is 404.
companies
array of BuyerPreviewCompanyInput
optional
1 to 50 companies, each with a name or a domain and an optional key echoed back.
perCompany
integer (int32)
optional
People per company, 1 to 3 (default 3).

Response 200

data is a BuyerPreviewModel.

Field
Type
Description
icp
array of string
companies
array of BuyerPreviewCompanyModel
Request
curl -X POST "https://api.ken.so/v1/contact-imports/buyer-previews" \
  -H "Authorization: Bearer $KEN_API_KEY" \
  -H "Ken-Version: 2026-11-02" \
  -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{
  "campaignId": 812,
  "companies": [
    {
      "key": "string",
      "name": "string",
      "domain": "example.com"
    }
  ],
  "perCompany": 1
}'
Response 200
{
  "success": true,
  "data": {
    "icp": [
      "string"
    ],
    "companies": [
      {
        "key": "string",
        "input": {
          "name": "string",
          "domain": "example.com"
        },
        "status": "string",
        "unmatchedReason": "string",
        "company": {
          "kenCompanyId": 812,
          "name": "string",
          "domain": "example.com",
          "employeeCount": 1,
          "industry": "string"
        },
        "signal": {
          "kind": "string",
          "label": "string",
          "at": "2026-09-23T15:02:11Z"
        },
        "people": [
          {
            "kenPersonId": 812,
            "fullName": "string",
            "title": "string",
            "seniorityLevel": "string",
            "function": "string",
            "linkedInUrl": "https://example.com",
            "reason": "string",
            "weakMatch": true,
            "contact": {}
          }
        ]
      }
    ]
  }
}

Start a contact import and return its job

POST/v1/contact-importscontact-imports.create

Try it on developer.ken.so

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) 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. Its optional form field emailsFoundByKen=true (staff only, otherwise 403 forbidden) stamps every imported email found_by_ken, so the cross-workspace Kenso DB lookup may serve it. 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 sourcegoogle_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 sourceairtable (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 sourcesaved_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: 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.

Parameters

Name
In
Type
Required
Description
Ken-Version
In: header
string, one of: 2026-11-02, 1.1
required
The API version: a dated version (YYYY-MM-DD), or 1.1, another name for the first dated version. Without it the request uses your API key's pinned version; an unpinned key reaches the legacy shape of the same path. 1.0 selects the legacy shape explicitly.
X-Ken-Workspace-Id
In: header
integer (int64)
optional
Workspace to act in. Required when your credential can reach more than one workspace (otherwise 400 workspace_required).

Request body

application/json, required.

Field
Type
Required
Description
campaignId
integer (int64)
optional
The campaign to import into. Required with target campaign; ignored with target people.
source
string
optional
campaign, segment, search, people, event, linkedin_connections, sales_navigator, linkedin_url, google_sheets, airtable or saved_list. A source behind a feature that is off answers like an unknown source (or 404 for linkedin_url).
target
string
optional
campaign (the default) adds the rows to campaignId as contacts; people runs them through person intake as workspace people.
sourceCampaignId
integer (int64)
optional
source campaign: the campaign to copy contacts from.
segmentId
integer (int64)
optional
Not supported by source search or linkedin_connections (400): their contacts go to the campaign's default segment.
contacts
array of SearchContactDTO
optional
source search: 1 to 1,000 contact rows.
personIds
array of integer (int64)
optional
source people: 1 to 1,000 workspace people to add as contacts.
eventId
integer (int64)
optional
source event: the outbound event whose attendees become contacts.
roles
array of string
optional
source event: the roles to import (attendee, exhibitor, speaker, sponsor), default every role. source saved_list with a company list: 1 to 20 job titles to find people for.
includeAccounts
boolean
optional
source event: true also imports rows whose company is a workspace account. Default false.
connectionIds
array of integer (int64)
optional
source linkedin_connections: 1 to 1,000 LinkedIn connection ids from GET /v1/linkedin-connections.
linkedinAccountId
integer (int64)
optional
source sales_navigator or linkedin_url: a LinkedIn seat the caller connected, which reads url.
url
string
optional
source sales_navigator: a Sales Navigator lead search or lead list URL. source linkedin_url: a LinkedIn post, people search, Sales Navigator search or Sales Navigator lead list URL.
spreadsheetId
string
optional
source google_sheets: the spreadsheet id, or its Google Sheets URL.
sheet
string
optional
source google_sheets: the sheet (tab) title. Omitted reads the first sheet.
baseId
string
optional
source airtable: the Airtable base id (app...).
tableId
string
optional
source airtable: the table id (tbl...) or name. Its field names are the header row.
viewId
string
optional
source airtable: a view id (viw...) or name; only its records are imported. Omitted reads the whole table.
columnMappings
array of CsvColumnMapping
optional
source google_sheets or airtable: which column feeds which contact field, as for a CSV upload. Omitted maps the header row by the import's aliases. Refused with source saved_list.
maxLeads
integer (int32)
optional
source linkedin_url: default 500; at most 2,000 for a post, 1,000 for a people search, 2,500 for Sales Navigator.
postKinds
array of string
optional
source linkedin_url, post URLs only: reaction, comment, or both (default).
savedListId
integer (int64)
optional
source saved_list: the saved list whose members are imported.
memberFilters
ContactImportMemberFiltersBody
optional
memberFilters of a saved_list import. A null or empty value is no filter.
findPeople
boolean
optional
source saved_list: must be absent or false.
importLimit
integer (int32)
optional
source campaign or search: import at most this many contacts. Greater than 0.
importOffset
integer (int32)
optional
source campaign: skip this many source contacts first. 0 or more.
onlyImportQualifiedContacts
boolean
optional
source campaign: only the source's qualified contacts. Default false.
onlyImportValidContacts
boolean
optional
source campaign: only contacts with a valid email. Default false.
deduplicateCampaignIds
array of integer (int64)
optional
source campaign: leave out people already in these campaigns of the workspace. 1 to 50 ids.
deduplicationMode
string
optional
source campaign: none, by_email (the default) or by_data (first name, last name and company).
allowReimport
boolean
optional
source campaign: repeat a finished copy from the same source campaign. Default false.
copyDataEnrichment
boolean
optional
source campaign: keep the source contacts' data enrichment results (LinkedIn profile, posts). Default false.
copyEmailEnrichment
boolean
optional
source campaign or segment: keep the source contacts' email finder results. Default false.
triggerPostProcessing
boolean
optional
source search: start enrichment and the AI flows after the import. Default true.
tagIds
array of integer (int64)
optional
source search: workspace contact tags to put on every imported contact. 1 to 50 ids.
sourceSegmentId
integer (int64)
optional
source segment: the workspace segment to copy into campaignId as a new segment. Required.
copyQualification
boolean
optional
source segment: keep the source contacts' AI qualification results. Default false.
copyEmailVerification
boolean
optional
source segment: keep the source contacts' email verification results. Default false.

Response 202

data is a JobModel.

Field
Type
Description
id
integer (int64)
type
string
status
string
progress
JobProgressModel
resource
JobResourceModel
createdAt
string (date-time)
downloadUrl
string
error
JobErrorModel
result
object
Upstream payload for a scraping job get. Omitted for other types and for lists.
domainsListed
integer (int32)
Listed-domain count stored by a completed domain health sweep. Omitted until that count exists.
excludedLibraryContacts
integer (int32)
Contacts from a Kenso library list a contact export left out (they are never exported), including 0. Only on contact_export jobs of kind contacts, campaign and advanced; omitted otherwise.
inboxImport
InboxImportJobModel
Counts and per-row results of an inbox_import job. Passwords are never returned.
personImport
PersonImportJobModel
Counts of a contact_import job with target people: each row created a person, updated (matched) one, or was skipped (no email, LinkedIn URL, or name with a business company domain, or intake refused it).
skipped
ContactImportSkipsModel
Rows a campaign contact_import job skipped, by reason. The duplicate counts name the rule that dropped the row: a contact already in this campaign, a contact in another campaign of the workspace (the campaign dedups against other campaigns), an earlier row of the same import, or the campaign's recently-emailed window.
Request
curl -X POST "https://api.ken.so/v1/contact-imports" \
  -H "Authorization: Bearer $KEN_API_KEY" \
  -H "Ken-Version: 2026-11-02" \
  -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{
  "campaignId": 812,
  "source": "string",
  "target": "string",
  "sourceCampaignId": 812,
  "segmentId": 812,
  "contacts": [
    {
      "firstName": "string",
      "lastName": "string",
      "fullName": "string",
      "title": "string",
      "headline": "string",
      "location": "string",
      "linkedInUrl": "https://example.com",
      "companyName": "string",
      "companyDomain": "example.com",
      "companyLinkedInUrl": "https://example.com",
      "companyIndustry": "string",
      "companyEmployeeCount": 1,
      "companyDescription": "string",
      "companyType": "string",
      "companyFoundedYear": 1,
      "companyLocation": "string",
      "companySpecialties": "string",
      "summary": "string",
      "experiencesJson": "string",
      "educationJson": "string",
      "certificationsJson": "string",
      "inferredSkills": "string",
      "languages": "string",
      "lastJobDescription": "string",
      "kenCompanyId": 812,
      "profilePhotoUrl": "https://example.com",
      "city": "string",
      "state": "string",
      "country": "string",
      "countryIso2": "string",
      "seniorityLevel": "string",
      "function": "string",
      "isDecisionMaker": true,
      "yearsOfExperience": 0.5,
      "yearsInCurrentRole": 0.5,
      "yearsInCurrentCompany": 0.5,
      "changedJobRecently": true,
      "jobChangeDate": "2026-09-23T15:02:11Z",
      "companyFounded": 1,
      "headquartersCity": "string",
      "headquartersState": "string",
      "headquartersCountry": "string",
      "companySize": "string",
      "companyLogoUrl": "https://example.com",
      "companyFollowers": 1,
      "companyTotalFunding": 1,
      "companyLastFundingRoundType": "string",
      "companyLastFundingAmount": 1,
      "currentExperiences": [
        {
          "companyId": 812,
          "companyName": "string",
          "companyUrl": "https://example.com",
          "companyDomain": "example.com",
          "companyEmployeeCount": 1,
          "title": "string",
          "startDate": "2026-09-23T15:02:11Z"
        }
      ]
    }
  ],
  "personIds": [
    812
  ],
  "eventId": 812,
  "roles": [
    "string"
  ],
  "includeAccounts": true,
  "connectionIds": [
    812
  ],
  "linkedinAccountId": 812,
  "url": "https://example.com",
  "spreadsheetId": "string",
  "sheet": "string",
  "baseId": "string",
  "tableId": "string",
  "viewId": "string",
  "columnMappings": [
    {
      "csvColumn": "string",
      "targetField": "string",
      "customVariableName": "string",
      "useAsAiContext": true,
      "isRequiredForSending": true
    }
  ],
  "maxLeads": 1,
  "postKinds": [
    "string"
  ],
  "savedListId": 812,
  "memberFilters": {
    "country": "string",
    "seniority": "string",
    "title": "string",
    "industry": "string",
    "headcountBand": "string",
    "hasSendableEmail": true
  },
  "findPeople": true,
  "importLimit": 1,
  "importOffset": 1,
  "onlyImportQualifiedContacts": true,
  "onlyImportValidContacts": true,
  "deduplicateCampaignIds": [
    812
  ],
  "deduplicationMode": "string",
  "allowReimport": true,
  "copyDataEnrichment": true,
  "copyEmailEnrichment": true,
  "triggerPostProcessing": true,
  "tagIds": [
    812
  ],
  "sourceSegmentId": 812,
  "copyQualification": true,
  "copyEmailVerification": true
}'
Response 202
{
  "success": true,
  "data": {
    "id": 812,
    "type": "string",
    "status": "string",
    "progress": {
      "completed": 1,
      "total": 1
    },
    "resource": {
      "type": "string",
      "id": 812
    },
    "createdAt": "2026-09-23T15:02:11Z",
    "downloadUrl": "https://example.com",
    "error": {
      "code": "string",
      "message": "string"
    },
    "result": null,
    "domainsListed": 1,
    "excludedLibraryContacts": 1,
    "inboxImport": {
      "connectedCount": 1,
      "failedCount": 1,
      "skippedCount": 1,
      "startedAt": "2026-09-23T15:02:11Z",
      "updatedAt": "2026-09-23T15:02:11Z",
      "completedAt": "2026-09-23T15:02:11Z",
      "rows": [
        {
          "index": 1,
          "email": "[email protected]",
          "status": "string",
          "inboxId": 812,
          "errorCode": "string",
          "message": "string"
        }
      ]
    },
    "personImport": {
      "source": "string",
      "createdCount": 1,
      "updatedCount": 1,
      "skippedCount": 1,
      "startedAt": "2026-09-23T15:02:11Z",
      "completedAt": "2026-09-23T15:02:11Z"
    },
    "skipped": {
      "duplicateInCampaign": 1,
      "duplicateInOtherCampaign": 1,
      "duplicateInSource": 1,
      "alreadyContacted": 1,
      "blocklist": 1,
      "failedValidation": 1,
      "salesNavigatorProfileUnresolved": 1,
      "other": 1,
      "missingKey": 1,
      "invalidEmail": 1,
      "noMx": 1,
      "catchAll": 1,
      "personalEmail": 1,
      "roleAddress": 1,
      "companyContacted": 1,
      "companyCap": 1
    }
  }
}

Suggest the Kenso field of every column of an import file from its header and sample rows

POST/v1/contact-imports/scanscontact-imports.scan

Needs the csv_import_check feature on your workspace. Until it is on, calls answer 404.

Try it on developer.ken.so

Body: source (required: csv, google_sheets or airtable), target (campaign, the default, or people), campaignId (required with target campaign: a campaign of the workspace, else 404), headers (the header row, 1 to 200 names of at most 500 characters) and sampleRows (up to 200 rows; a short row reads as blanks, extra cells are ignored, each cell at most 2,000 characters). keys (optional) holds the key columns of every row for the List check: rowCount (data rows in the file), cleanup (default true: each value first goes through the import's cleaner) and one list per mapped key field (email, profileUrl, companyDomain, companyName, companyLinkedinUrl, firstName, lastName, fullName, title, emailValidity), index-aligned, plus otherEmails (each further column mapped to email, up to 10 lists) and otherEmailValidity (the validity column mapped right after each, null where none). With target campaign every list sent has the same length, 1 to 25,000, and rowCount is at least that length (else 400 on keys); a larger file sends its first 25,000 rows and its check is truncated. A scan that sends keys skips the AI suggestions. Example: { "source": "csv", "campaignId": 42, "headers": ["Full Name", "Work contact"], "sampleRows": [["Ada Lovelace", "[email protected]"]], "keys": { "rowCount": 1, "fullName": ["Ada Lovelace"], "email": ["[email protected]"] } }. Returns 200 with columns, one per header in header order: index, csvColumn, targetField (a contact field key such as email, full_name or profile_url, custom_variable or skip), customVariableName (only for custom_variable), confidence (high: the header names the field or nearly every value is one; memory: this workspace mapped the header before; check: likely, confirm it; skip: not imported), reason (one short sentence), samples (up to 3 distinct values, each at most 120 characters) and cleanup (what the import's cleanup changes in the sample values, e.g. Cleans 3 of 10 sample values (casing, format), else null); listKind (company when no column is a person field and one is a company field, else people); and check, the List check, with keys and target campaign (else null): rows, scannedRows, truncated, cleanupApplied, always (what the import skips anyway: failedValidation, missingKey, blocklist, duplicateInFile, duplicateInCampaign, duplicateInOtherCampaign, first match wins), contacted (rows another campaign emailed inside the campaign's recently-emailed window), checks (optional fixes: invalidEmail, noMx, catchAll, personalEmail, roleAddress, companyContacted, companyCap), rowFlags ([rowIndex, mask]), noEmailRows, baselinePrior, bounce (expected, beforeFixes, band), willAdd, companyContactedDays, companyCap, domainsChecked, domainsTotal, titleMix and providerMix. Samples carry the 1-based data row; rows and row indexes are 0-based into the key lists. Kenso's own data only, no paid lookup. A field is suggested once, except email, email_validity and custom_variable. Target people suggests only the fields a people import stores: email, first_name, last_name, full_name, title, company_name, company_domain, phone_number and profile_url. Nothing is stored. Behind the csv_import_check flag (404 when off). Needs campaigns view and the import feature. Each call counts on the workspace's contactImportScans quota (30 a minute, 600 an hour, 5,000 a day; past it, 429 quota_exceeded with Retry-After).

Parameters

Name
In
Type
Required
Description
Ken-Version
In: header
string, one of: 2026-11-02, 1.1
required
The API version: a dated version (YYYY-MM-DD), or 1.1, another name for the first dated version. Without it the request uses your API key's pinned version; an unpinned key reaches the legacy shape of the same path. 1.0 selects the legacy shape explicitly.
X-Ken-Workspace-Id
In: header
integer (int64)
optional
Workspace to act in. Required when your credential can reach more than one workspace (otherwise 400 workspace_required).

Request body

application/json, optional.

Field
Type
Required
Description
source
string
optional
Required: csv, google_sheets or airtable.
target
string
optional
campaign (the default) or people (only the fields a people import takes).
campaignId
integer (int64)
optional
Required with target campaign: a campaign of the workspace.
headers
array of string
optional
The header row: 1 to 200 names, each at most 500 characters.
sampleRows
array of array of string
optional
Up to 200 data rows. A row shorter than the headers reads as blanks; extra cells are ignored. Each cell at most 2,000 characters.
keys
ContactImportScanKeys
optional

Response 200

data is a ContactImportScanModel.

Field
Type
Description
columns
array of ContactImportScanColumnModel
One per header, in header order.
listKind
string
company when no column maps to email, profile_url, first_name, last_name or full_name and one maps to company_name, company_domain or company_linkedin_url; otherwise people.
check
ContactImportListCheckModel
The List check: what the import skips anyway, the rows the campaign's recently-emailed window drops, and the optional fixes for risky rows. row in a sample is the 1-based data row number (header excluded); rows and the first number of each rowFlags pair are 0-based indexes into the key lists. A skipped row's reason (for a download of the skipped rows) is the first match in this order: its always bucket, then contacted when it is skipped, then the first check that is on, in checks order.
Request
curl -X POST "https://api.ken.so/v1/contact-imports/scans" \
  -H "Authorization: Bearer $KEN_API_KEY" \
  -H "Ken-Version: 2026-11-02" \
  -H "X-Ken-Workspace-Id: $KEN_WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{
  "source": "string",
  "target": "string",
  "campaignId": 812,
  "headers": [
    "string"
  ],
  "sampleRows": [
    [
      "string"
    ]
  ],
  "keys": {
    "rowCount": 1,
    "cleanup": true,
    "email": [
      "[email protected]"
    ],
    "profileUrl": [
      "https://example.com"
    ],
    "companyDomain": [
      "example.com"
    ],
    "companyName": [
      "string"
    ],
    "companyLinkedinUrl": [
      "https://example.com"
    ],
    "firstName": [
      "string"
    ],
    "lastName": [
      "string"
    ],
    "fullName": [
      "string"
    ],
    "title": [
      "string"
    ],
    "emailValidity": [
      "string"
    ],
    "otherEmails": [
      [
        "[email protected]"
      ]
    ],
    "otherEmailValidity": [
      [
        "string"
      ]
    ]
  }
}'
Response 200
{
  "success": true,
  "data": {
    "columns": [
      {
        "index": 1,
        "csvColumn": "string",
        "targetField": "string",
        "customVariableName": "string",
        "confidence": "string",
        "reason": "string",
        "samples": [
          "string"
        ],
        "cleanup": "string"
      }
    ],
    "listKind": "string",
    "check": {
      "rows": 1,
      "scannedRows": 1,
      "truncated": true,
      "cleanupApplied": true,
      "always": [
        {
          "key": "string",
          "label": "string",
          "count": 1,
          "samples": [
            {}
          ],
          "rows": [
            1
          ]
        }
      ],
      "contacted": {
        "count": 1,
        "days": 1,
        "skipByDefault": true,
        "samples": [
          {
            "row": 1,
            "value": "string",
            "reason": "string"
          }
        ],
        "rows": [
          1
        ]
      },
      "checks": [
        {
          "key": "string",
          "label": "string",
          "fixLabel": "string",
          "count": 1,
          "defaultOn": true,
          "prior": 0.5,
          "note": "string",
          "samples": [
            {}
          ]
        }
      ],
      "rowFlags": [
        [
          1
        ]
      ],
      "noEmailRows": [
        1
      ],
      "baselinePrior": 0.5,
      "bounce": {
        "expected": 0.5,
        "beforeFixes": 0.5,
        "band": "string"
      },
      "willAdd": 1,
      "companyContactedDays": 1,
      "companyCap": 1,
      "domainsChecked": 1,
      "domainsTotal": 1,
      "titleMix": [
        {
          "label": "string",
          "share": 1
        }
      ],
      "providerMix": [
        {
          "label": "string",
          "share": 1
        }
      ]
    }
  }
}