---
title: "POST /api/icps"
method: POST
path: "/api/icps"
tags: ["icps"]
---

# POST /api/icps

`POST /api/icps`

## Request

- Base URL: `https://app.heykairon.com` (the document declares no server; this is the origin it was published from)
- URL: `https://app.heykairon.com/api/icps`
- Auth: API key in header `x-api-key`

## Request body

- CreateIcpDto
  - `description` string
  - `companySalesNavCriteria` object
    - `category` 'people' | 'companies'
    - `keywords` string: LAST resort — matches noisily across all text. Prefer a structured filter for any title/industry/geography/size intent; use keywords only for a concept no filter captures. This is a BOOLEAN query, not a phrase: bare space-separated words are AND-ed (ALL must appear), so "fintech payments neobank" requires all three together and usually returns ZERO. For ALTERNATIVES, OR them, quote each, and ALWAYS wrap the OR group in parentheses: `("fintech" OR "payments" OR "neobank")`. The parentheses are MANDATORY — LinkedIn silently mis-parses an unparenthesized OR of 4+ terms and returns ~0. Quote multi-word phrases; combine groups with AND/NOT (e.g. `("fintech" OR "neobank") AND "payments"`). Operators must be UPPERCASE.
    - `filters` object[]: Id-resolved dimensions (location, industry, company, …). Same dimension repeated = OR; different dimensions = AND (each narrows the results).
      - `type` 'location' | 'industry' | 'function' | 'current_company' | 'past_company' | 'company_location' | 'school' | 'past_role' | 'groups' | 'persona' | 'technologies' | 'account_lists' | 'lead_lists' | 'postal_code', required: Which id-resolved dimension. People: location, industry, function, current_company, past_company, company_location, school, past_role, groups, persona, account_lists, lead_lists, postal_code. Companies: location, industry, technologies, account_lists, postal_code — the rest are people-only and are REFUSED here rather than quietly ignored. On a companies search the company IS the subject, so its location is plain `location` (`company_location` is accepted as a synonym for it, and means the EMPLOYER’s location only on a people search). `groups` is measured from YOU, like the saved lists: LinkedIn only knows the groups your own account has JOINED, so it works only on your OWN Sales Navigator seat and is otherwise refused, rather than answered about a different audience. To reach the people in a group, use `group_members` instead — it reads the group’s own member list on your own seat: any public group, or a members-only one your seat has joined.
      - `query` string, required: The value as a human-readable NAME (never an id). Phrase it the way LinkedIn names things — canonical industry names ("Financial Services", not "fintech"), real place names ("Greater Madrid Metropolitan Area" or the country). Resolution takes the best single typeahead match and echoes the resolved label back.
      - `mode` 'include' | 'exclude': "include" (default) requires the value; "exclude" filters it out.
    - `cursor` string: Pass the cursor from a prior result to page. It continues only the exact search that made it: send it with the SAME category and filters as the first page, or it is refused.
    - `limit` integer: Page size (max 100 for Sales Nav; default 10). One page costs one metered action WHATEVER its size, so raise it when you are filling a list rather than sampling.
    - `firstName` string
    - `lastName` string
    - `role` object: Current job-TITLE keywords (include/exclude) — the primary people filter. Use it for ALL title intent, NOT `keywords` and NOT `seniority`. Each include entry is a separate Boolean title clause LinkedIn ORs. Two kinds: (1) single-title pills — exact exec titles + founder, one entry per variant (e.g. "CEO","Chief Executive Officer","Founder","Fundador"); (2) ONE seniority-block pill for broad leadership roles: a marker AND a parenthesized OR-list of role synonyms, e.g. `<<SENIORITY_BLOCK_EN>> AND ("growth" OR "demand generation")`. The marker (one of <<SENIORITY_BLOCK_EN>> / <<SENIORITY_BLOCK_ES_EN>> / <<SENIORITY_BLOCK_PT_EN>>, by language) expands server-side to a broad leadership OR-block so you recall heads/VPs/directors/managers without enumerating titles. See the playbook for the full recipe. MATCHING: a pill matches a SET OF WORDS, not a phrase, and it matches a person across ALL of their CURRENT jobs. When you also scope by company, only their position AT THAT COMPANY is returned — so a hit whose shown title does not match your pills is usually a CORRECT match through a side venture, not a broken filter. `Founder`/`Co-Founder`/`Owner`/`CEO` anchored to a company list therefore mean "anyone there who founded or owns anything, anywhere"; to find who RUNS a company use `Gerente General`/`Director General`/`Managing Director`/`Country Manager`, and `linkedin_fetch` to check one odd hit.
      - `include` string[]
      - `exclude` string[]
    - `seniority` object: LinkedIn's coarse seniority ladder (cxo, vice_president, director, senior, …) — NOT job titles. Use only when you mean a seniority level; pair with `role` for "VP-level whose title contains X".
      - `include` string[]
      - `exclude` string[]
    - `tenure` object[]: Years at the current company in their current role, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
      - `min` number
      - `max` number
    - `tenureAtCompany` object[]: Total years at the current company, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
      - `min` number
      - `max` number
    - `tenureAtRole` object[]: Years in the current role, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
      - `min` number
      - `max` number
    - `companyHeadcount` string[]: Size band of the person's current employer (people search). For a company's OWN size use `headcount`. Approximate "startup" with small buckets (["1-10","11-50","51-200"]).
    - `companyType` string[]: Ownership type of the person's current employer (people search; e.g. privately_held, public_company). A companies search has no company-type facet.
    - `availableForWork` boolean: Signals availability for a new role (#OpenToWork, or no current position). true requires it, false excludes it. Screened after the search, on loaded profiles only; unknown never counts as available.
    - `minFollowerCount` integer: Minimum LinkedIn follower count for a person. Screened after the search, on loaded profiles only; a profile whose follower count we could not read never passes.
    - `postedWithinDays` integer: Require an own post within this many days (1-365). Screened last, on loaded profiles only, and it costs one extra LinkedIn read; a person with no readable recent post never passes.
    - `profileLanguage` string[]: Profile languages as 2-letter ISO codes (e.g. ["en","es"]).
    - `networkDistance` union[]: Connection degree from the operator: 1 / 2 / 3 — valid for BOTH people and companies. The "GROUP" value (shared group) is PEOPLE only; do not use it on a companies search. Answered only on the operator's OWN Sales Navigator seat. A degree IS a structured filter, so with the org's OWN Sales Navigator seat this is the right place to ask for "my 1st-degree X" — you keep the `role` / `seniority` precision classic search cannot express. Do NOT downgrade to `search_people` for it. `identity_get` says whether it can be answered here (`capabilities.viewerRelativeFilters`) — but do not gate the SEARCH on it: every other filter runs either way, so drop this one and keep the rest rather than leaving. When it cannot be answered the search says so and refuses THIS filter only; classic `search_people` with `networkDistance` is then the route, because it always runs on the operator's own seat. Never treat a Sales-Nav hit as connected: confirm before building a campaign that sends a direct message.
      - union
        - number
        - number
        - number
        - string
    - `connectionsOf` string[]: LinkedIn provider ids (`ACoAA…`, from people.suggest — NOT numeric member ids) whose connections to search within. Aimed at someone you are NOT connected to, this returns your MUTUAL connections with them — the people who could introduce you — matching the `shared_connections_count` on their profile. `networkDistance` defaults to [1] and must STAY [1] for such a target: at any other degree Sales Navigator silently discards this filter and answers with your entire 2nd-degree network, which looks like a real result. The one exception is a target you ARE connected to, where [2] validly returns everyone they know that you do not. Looking for an introduction to someone? Read the `warm-intro` skill first — this filter is one step of it, and the rest decides which of the people it returns can actually help. Like `networkDistance` this is relative to the SEAT, and works here whenever `identity_get`'s `capabilities.viewerRelativeFilters` is true. When it is not, the search refuses THIS filter and nothing else — classic `search_people` is the route for the introduction hunt, while every other Sales Navigator filter still runs here.
    - `changedJobs` boolean: Spotlight: only people who recently changed jobs.
    - `postedOnLinkedin` boolean
    - `mentionedInNews` boolean
    - `followingYourCompany` boolean
    - `pastColleague` boolean: Spotlight, relative to YOU: only people you actually worked with. Combined with `connectionsOf` it answers "which of this person's connections are my own former colleagues" — the shortest warm path there is. Expect a very small result; that is the point, not a misfire.
    - `sharedExperiences` boolean: Spotlight, relative to YOU: only people who share a school, employer or group with you. The wider net beneath `pastColleague`, and the same combination with `connectionsOf` applies. The response never says WHICH experience is shared — membership in the result is the signal; load a profile to name the overlap.
    - `viewedYourProfileRecently` boolean
    - `viewedProfileRecently` boolean
    - `messagedRecently` boolean
    - `includeSavedLeads` boolean
    - `includeSavedAccounts` boolean
    - `hasJobOffers` boolean: Company search: only companies currently hiring.
    - `headcount` string[]: The company's OWN size band (companies search; the analog of people-search `companyHeadcount`). Approximate "startup" with small buckets (["1-10","11-50","51-200"]).
    - `headcountGrowth` object: Company headcount growth rate as a {min,max} percentage range.
      - `min` number
      - `max` number
    - `annualRevenue` object: Company annual revenue {currency (ISO 4217), min, max} in millions. Both min AND max are required and MUST be breakpoints: 0, 0.2, 1, 2.5, 5, 10, 20, 50, 100, 500, 1000, 1001.
      - `currency` string, required
      - `min` number, required
      - `max` number, required
    - `followersCount` object[]: COMPANY followers only — a companies search. Search has no filter for a PERSON's followers; check that with the ICP. {min,max} ranges. Breakpoints — min: 1, 51, 101, 1001, 5001; max: 50, 100, 1000, 5000.
      - `min` number
      - `max` number
    - `fortune` object[]: Fortune ranking {min,max} ranges (e.g. Fortune 500). Breakpoints — min: 0, 51, 101, 251; max: 50, 100, 250, 500.
      - `min` number
      - `max` number
    - `recentActivities` string[]: Company signals: senior_leadership_changes and/or funding_events.
    - `departmentHeadcount` object
      - `department` string[], required
      - `min` number
      - `max` number
    - `departmentHeadcountGrowth` object
      - `department` string[], required
      - `min` number
      - `max` number
    - `savedAccounts` string[]
    - `savedSearchId` string
    - `recentSearchId` string
    - `lastViewedAt` number
    - `withinArea` number
  - `peopleSalesNavCriteria` object
    - `category` 'people' | 'companies'
    - `keywords` string: LAST resort — matches noisily across all text. Prefer a structured filter for any title/industry/geography/size intent; use keywords only for a concept no filter captures. This is a BOOLEAN query, not a phrase: bare space-separated words are AND-ed (ALL must appear), so "fintech payments neobank" requires all three together and usually returns ZERO. For ALTERNATIVES, OR them, quote each, and ALWAYS wrap the OR group in parentheses: `("fintech" OR "payments" OR "neobank")`. The parentheses are MANDATORY — LinkedIn silently mis-parses an unparenthesized OR of 4+ terms and returns ~0. Quote multi-word phrases; combine groups with AND/NOT (e.g. `("fintech" OR "neobank") AND "payments"`). Operators must be UPPERCASE.
    - `filters` object[]: Id-resolved dimensions (location, industry, company, …). Same dimension repeated = OR; different dimensions = AND (each narrows the results).
      - `type` 'location' | 'industry' | 'function' | 'current_company' | 'past_company' | 'company_location' | 'school' | 'past_role' | 'groups' | 'persona' | 'technologies' | 'account_lists' | 'lead_lists' | 'postal_code', required: Which id-resolved dimension. People: location, industry, function, current_company, past_company, company_location, school, past_role, groups, persona, account_lists, lead_lists, postal_code. Companies: location, industry, technologies, account_lists, postal_code — the rest are people-only and are REFUSED here rather than quietly ignored. On a companies search the company IS the subject, so its location is plain `location` (`company_location` is accepted as a synonym for it, and means the EMPLOYER’s location only on a people search). `groups` is measured from YOU, like the saved lists: LinkedIn only knows the groups your own account has JOINED, so it works only on your OWN Sales Navigator seat and is otherwise refused, rather than answered about a different audience. To reach the people in a group, use `group_members` instead — it reads the group’s own member list on your own seat: any public group, or a members-only one your seat has joined.
      - `query` string, required: The value as a human-readable NAME (never an id). Phrase it the way LinkedIn names things — canonical industry names ("Financial Services", not "fintech"), real place names ("Greater Madrid Metropolitan Area" or the country). Resolution takes the best single typeahead match and echoes the resolved label back.
      - `mode` 'include' | 'exclude': "include" (default) requires the value; "exclude" filters it out.
    - `cursor` string: Pass the cursor from a prior result to page. It continues only the exact search that made it: send it with the SAME category and filters as the first page, or it is refused.
    - `limit` integer: Page size (max 100 for Sales Nav; default 10). One page costs one metered action WHATEVER its size, so raise it when you are filling a list rather than sampling.
    - `firstName` string
    - `lastName` string
    - `role` object: Current job-TITLE keywords (include/exclude) — the primary people filter. Use it for ALL title intent, NOT `keywords` and NOT `seniority`. Each include entry is a separate Boolean title clause LinkedIn ORs. Two kinds: (1) single-title pills — exact exec titles + founder, one entry per variant (e.g. "CEO","Chief Executive Officer","Founder","Fundador"); (2) ONE seniority-block pill for broad leadership roles: a marker AND a parenthesized OR-list of role synonyms, e.g. `<<SENIORITY_BLOCK_EN>> AND ("growth" OR "demand generation")`. The marker (one of <<SENIORITY_BLOCK_EN>> / <<SENIORITY_BLOCK_ES_EN>> / <<SENIORITY_BLOCK_PT_EN>>, by language) expands server-side to a broad leadership OR-block so you recall heads/VPs/directors/managers without enumerating titles. See the playbook for the full recipe. MATCHING: a pill matches a SET OF WORDS, not a phrase, and it matches a person across ALL of their CURRENT jobs. When you also scope by company, only their position AT THAT COMPANY is returned — so a hit whose shown title does not match your pills is usually a CORRECT match through a side venture, not a broken filter. `Founder`/`Co-Founder`/`Owner`/`CEO` anchored to a company list therefore mean "anyone there who founded or owns anything, anywhere"; to find who RUNS a company use `Gerente General`/`Director General`/`Managing Director`/`Country Manager`, and `linkedin_fetch` to check one odd hit.
      - `include` string[]
      - `exclude` string[]
    - `seniority` object: LinkedIn's coarse seniority ladder (cxo, vice_president, director, senior, …) — NOT job titles. Use only when you mean a seniority level; pair with `role` for "VP-level whose title contains X".
      - `include` string[]
      - `exclude` string[]
    - `tenure` object[]: Years at the current company in their current role, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
      - `min` number
      - `max` number
    - `tenureAtCompany` object[]: Total years at the current company, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
      - `min` number
      - `max` number
    - `tenureAtRole` object[]: Years in the current role, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
      - `min` number
      - `max` number
    - `companyHeadcount` string[]: Size band of the person's current employer (people search). For a company's OWN size use `headcount`. Approximate "startup" with small buckets (["1-10","11-50","51-200"]).
    - `companyType` string[]: Ownership type of the person's current employer (people search; e.g. privately_held, public_company). A companies search has no company-type facet.
    - `availableForWork` boolean: Signals availability for a new role (#OpenToWork, or no current position). true requires it, false excludes it. Screened after the search, on loaded profiles only; unknown never counts as available.
    - `minFollowerCount` integer: Minimum LinkedIn follower count for a person. Screened after the search, on loaded profiles only; a profile whose follower count we could not read never passes.
    - `postedWithinDays` integer: Require an own post within this many days (1-365). Screened last, on loaded profiles only, and it costs one extra LinkedIn read; a person with no readable recent post never passes.
    - `profileLanguage` string[]: Profile languages as 2-letter ISO codes (e.g. ["en","es"]).
    - `networkDistance` union[]: Connection degree from the operator: 1 / 2 / 3 — valid for BOTH people and companies. The "GROUP" value (shared group) is PEOPLE only; do not use it on a companies search. Answered only on the operator's OWN Sales Navigator seat. A degree IS a structured filter, so with the org's OWN Sales Navigator seat this is the right place to ask for "my 1st-degree X" — you keep the `role` / `seniority` precision classic search cannot express. Do NOT downgrade to `search_people` for it. `identity_get` says whether it can be answered here (`capabilities.viewerRelativeFilters`) — but do not gate the SEARCH on it: every other filter runs either way, so drop this one and keep the rest rather than leaving. When it cannot be answered the search says so and refuses THIS filter only; classic `search_people` with `networkDistance` is then the route, because it always runs on the operator's own seat. Never treat a Sales-Nav hit as connected: confirm before building a campaign that sends a direct message.
      - union
        - number
        - number
        - number
        - string
    - `connectionsOf` string[]: LinkedIn provider ids (`ACoAA…`, from people.suggest — NOT numeric member ids) whose connections to search within. Aimed at someone you are NOT connected to, this returns your MUTUAL connections with them — the people who could introduce you — matching the `shared_connections_count` on their profile. `networkDistance` defaults to [1] and must STAY [1] for such a target: at any other degree Sales Navigator silently discards this filter and answers with your entire 2nd-degree network, which looks like a real result. The one exception is a target you ARE connected to, where [2] validly returns everyone they know that you do not. Looking for an introduction to someone? Read the `warm-intro` skill first — this filter is one step of it, and the rest decides which of the people it returns can actually help. Like `networkDistance` this is relative to the SEAT, and works here whenever `identity_get`'s `capabilities.viewerRelativeFilters` is true. When it is not, the search refuses THIS filter and nothing else — classic `search_people` is the route for the introduction hunt, while every other Sales Navigator filter still runs here.
    - `changedJobs` boolean: Spotlight: only people who recently changed jobs.
    - `postedOnLinkedin` boolean
    - `mentionedInNews` boolean
    - `followingYourCompany` boolean
    - `pastColleague` boolean: Spotlight, relative to YOU: only people you actually worked with. Combined with `connectionsOf` it answers "which of this person's connections are my own former colleagues" — the shortest warm path there is. Expect a very small result; that is the point, not a misfire.
    - `sharedExperiences` boolean: Spotlight, relative to YOU: only people who share a school, employer or group with you. The wider net beneath `pastColleague`, and the same combination with `connectionsOf` applies. The response never says WHICH experience is shared — membership in the result is the signal; load a profile to name the overlap.
    - `viewedYourProfileRecently` boolean
    - `viewedProfileRecently` boolean
    - `messagedRecently` boolean
    - `includeSavedLeads` boolean
    - `includeSavedAccounts` boolean
    - `hasJobOffers` boolean: Company search: only companies currently hiring.
    - `headcount` string[]: The company's OWN size band (companies search; the analog of people-search `companyHeadcount`). Approximate "startup" with small buckets (["1-10","11-50","51-200"]).
    - `headcountGrowth` object: Company headcount growth rate as a {min,max} percentage range.
      - `min` number
      - `max` number
    - `annualRevenue` object: Company annual revenue {currency (ISO 4217), min, max} in millions. Both min AND max are required and MUST be breakpoints: 0, 0.2, 1, 2.5, 5, 10, 20, 50, 100, 500, 1000, 1001.
      - `currency` string, required
      - `min` number, required
      - `max` number, required
    - `followersCount` object[]: COMPANY followers only — a companies search. Search has no filter for a PERSON's followers; check that with the ICP. {min,max} ranges. Breakpoints — min: 1, 51, 101, 1001, 5001; max: 50, 100, 1000, 5000.
      - `min` number
      - `max` number
    - `fortune` object[]: Fortune ranking {min,max} ranges (e.g. Fortune 500). Breakpoints — min: 0, 51, 101, 251; max: 50, 100, 250, 500.
      - `min` number
      - `max` number
    - `recentActivities` string[]: Company signals: senior_leadership_changes and/or funding_events.
    - `departmentHeadcount` object
      - `department` string[], required
      - `min` number
      - `max` number
    - `departmentHeadcountGrowth` object
      - `department` string[], required
      - `min` number
      - `max` number
    - `savedAccounts` string[]
    - `savedSearchId` string
    - `recentSearchId` string
    - `lastViewedAt` number
    - `withinArea` number
  - `companyQualificationConditions` object[]
    - `id` string
    - `description` string, required
    - `webSearch` boolean
    - `siteKeywords` object
      - `include` string[]
      - `exclude` string[]
  - `peopleQualificationConditions` object[]
    - `id` string
    - `description` string, required
    - `webSearch` boolean
    - `siteKeywords` object
      - `include` string[]
      - `exclude` string[]
  - `strictFilters` boolean
  - `waiveMissingCompany` boolean
  - `name` string, required

## Response `201`

---

[API](https://skmtc.dev/heykairon/apis/kairon-api.md) · [All operations](https://skmtc.dev/heykairon/apis/kairon-api/llms.txt) · [OpenAPI document](https://skmtc.dev/heykairon/apis/kairon-api/revisions/35de550d5938?raw)
