---
title: "PUT /api/lead-lists/{leadListId}/source"
method: PUT
path: "/api/lead-lists/{leadListId}/source"
tags: ["sources"]
---

# PUT /api/lead-lists/{leadListId}/source

`PUT /api/lead-lists/{leadListId}/source`

## 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/lead-lists/{leadListId}/source`
- Auth: API key in header `x-api-key`

## Path parameters

- `leadListId` string, required

## Request body

- SetSourceBodyDto
  - `icpId` string: The ICP every new person must pass before joining the list, from icp_list. Its ACTIVE version is the check, so revising the ICP changes future runs. Omit it or pass null for no ICP: then everyone the signals find is added. This is a full replacement, so on a list that HAS an ICP, omitting it removes the check — send back the `icp.id` source_get gave you to keep it. Six signals also SEARCH with the ICP and need one: lookalike, changed_jobs, posted_recently, company_raised_funds, company_leadership_change, company_hiring. Changing the ICP restarts those six from the top of their feed; the others keep their place.
  - `signals` union[], required: The COMPLETE set of signals this source should carry — at least one. Any signal not listed here is removed. Echo each kept signal with the `id` source_get gave it, or it is treated as new and restarts from the top of its feed.
    - union
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who recently changed jobs. Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId).
        - `config` object: No configuration — this signal takes none.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who posted recently. Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId).
        - `config` object: No configuration — this signal takes none.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who viewed the seat’s profile. Needs a paid seat.
        - `config` object: No configuration — this signal takes none.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who recently accepted a connection. Any seat.
        - `config` object: No configuration — this signal takes none.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: Buyers at companies that recently raised funding. Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId). Takes no config — both hops read the targeted ICP.
        - `config` object: No configuration — this signal takes none.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: Buyers at companies with a recent leadership change. Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId). Takes no config — both hops read the targeted ICP.
        - `config` object: No configuration — this signal takes none.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People matching the targeted ICP, a few every day, with no event required — the signal to add when a list must keep growing while the event signals are quiet. Needs an ICP with Sales Navigator criteria on at least one side (an ICP with none is refused). Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId). Takes no config — it walks the ICP’s own search. Unlike the event signals it carries no "why now", so a lead it appends gives the message writer less to open on.
        - `config` object: No configuration — this signal takes none.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who published a post matching a query. Any seat. May appear MORE THAN ONCE on a source — one per query.
        - `config` object, required: The query this signal watches for.
          - `keyword` string, required: The boolean search query the posts search runs, e.g. "hiring SDR" OR "sales lead".
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who ENGAGED with posts matching a query — the engagers, not the authors. Any seat. May appear more than once.
        - `config` object, required: The query this signal watches for.
          - `keyword` string, required: The boolean search query the posts search runs, e.g. "hiring SDR" OR "sales lead".
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who engaged with a chosen profile’s posts. Any seat. May appear more than once — one per watched profile.
        - `config` object, required: Whose engagement this signal surfaces.
          - `target` object, required: The profile being watched.
            - `kind` 'person' | 'company', required: Whether the watched profile is a person or a company.
            - `url` string, required: The LinkedIn URL of the profile whose engagers this surfaces.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who commented on or reacted to ONE named post. Any seat. May appear more than once — one per watched post. Short-lived by nature: a post stops gathering engagement after a few days, and this signal then goes quiet on its own.
        - `config` object, required: The post this signal watches.
          - `url` string, required: The LinkedIn permalink of the post whose engagers this surfaces.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: People who FOLLOW a company page the source’s seat administers — an opted-in audience. Walks the whole follower list once, a few pages a day, then keeps picking up new followers. Any seat, but only for a page that seat runs. May appear more than once — one per page.
        - `config` object, required: The company page this signal walks.
          - `url` string, required: The LinkedIn company page link (https://www.linkedin.com/company/<slug>). It must be a page the source’s seat ADMINISTERS — LinkedIn shows followers to admins only, and the save is refused otherwise.
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: Buyers at companies that just published a job posting matching a search — the hiring signal. The people search at each company uses the ICP’s people criteria. Both searches run on Kairon’s Seat Pool, so no seat of your own is needed — unless the job search sets inYourNetwork, or the ICP’s people side has a viewer-relative filter (that one needs a seat with its own Sales Navigator). Either case reads on the list’s seat (seatId). May appear more than once — one per search (e.g. one per country).
        - `config` object, required: The job search to watch: every job_search market-sweep filter. `region` (one country, plain language) is REQUIRED, and so is at least one of `roles`, `keywords`, `functions`, `industries` — `roles` (job titles) is the sharpest. LinkedIn has no language filter: ask for Spanish-speaking roles with `keywords` such as "español". Checked with one job search when saved: a region or term that matches nothing on LinkedIn is refused.
          - `keywords` string: What the company is hiring FOR — a product, stack or role ("Oracle", "NetSuite", "SAP HCM"). This is the market sweep: it returns every company hiring that profile, so it DISCOVERS companies rather than filtering ones you already hold. Requires `region` unless `company` is also set. A product name usually needs no translation — "Oracle" catches Portuguese and Spanish postings alike.
          - `region` string, required: The ONE country to watch, in plain language ("Brazil") or as a LinkedIn geo id. Required: without it LinkedIn silently searches only the seat's own country. Watch another country with another hiring signal.
          - `datePosted` integer: How many days back to look. A sweep defaults to 30 days, which is almost always right: LinkedIn removes a posting when the role closes, so 90 days measured 170 postings against 151 at 30 — to find MORE companies repeat the sweep in a few weeks rather than widening it. A COMPANY CHECK has no default window: "is this company hiring" is asked of everything still open, so an empty answer means "not hiring" rather than "nothing lately".
          - `roles` string[]: Job TITLES from LinkedIn's own taxonomy, in plain language ("Product Analyst", "Head of Growth") — resolved to title ids for you and echoed back. This is the sharpest filter this tool has and usually beats `keywords`: measured on the same Argentina sweep, `keywords: "Product Analyst"` returned 1241 postings while `roles: ["Product Analyst"]` returned 17, because keywords match anywhere in a posting while a title id matches the ROLE. LinkedIn treats several titles as OR — pass the variants you mean ("Product Analyst", "Senior Product Analyst").
          - `functions` string[]: Job FUNCTIONS — the department a role belongs to ("Product Management", "Engineering", "Marketing"), resolved to LinkedIn ids and echoed back. Much wider than `roles`: Product Management alone returned 207 postings where the "Product Analyst" title returned 17. Reach for it to size a department across a market; reach for `roles` to find one job.
          - `industries` string[]: The HIRING COMPANY's industry, in plain language ("Financial Services", "Software Development") — resolved to LinkedIn ids and echoed back. This filters by the employer, not by the role, so it is how a sweep is aimed at the market you actually sell to.
          - `seniority` string[]: LinkedIn's EXPERIENCE LEVEL on the posting. This is the ONLY way to ask for seniority — putting "Senior" in `keywords` does the opposite of what it looks like, because LinkedIn ORs the words: the same sweep went from 1241 hits to 2480, wider, not narrower. Several levels are OR ("senior and above" is `["mid_senior","director","executive"]`, measured 675 against a 1241 baseline).
          - `jobType` string[]: LinkedIn's JOB TYPE — the employment arrangement. Several are OR.
          - `presence` string[]: On-site / hybrid / remote, as the posting declares it. Several are OR.
          - `underTenApplicants` boolean: LinkedIn's UNDER 10 APPLICANTS. The sharpest recency filter there is — it says the posting is not just recent but still UNWORKED, and it cut the same 1241-hit sweep to 127. Use it when the value of the signal is getting there first.
          - `inYourNetwork` boolean: LinkedIn's IN YOUR NETWORK — only companies where the seat already has a connection. Warm ground: it cut the same sweep from 1241 to 265, and every hit comes with a path in.
          - `easyApply` boolean: LinkedIn's EASY APPLY.
          - `hasVerifications` boolean: LinkedIn's HAS VERIFICATIONS — the poster verified their identity.
          - `fairChanceEmployer` boolean: LinkedIn's FAIR CHANCE EMPLOYER.
          - `benefits` string[]: LinkedIn's BENEFITS, as the posting declares them. Several are OR.
          - `commitments` string[]: LinkedIn's COMMITMENTS, as the company declares them. Several are OR.
          - `minimumSalary` union: LinkedIn's SALARY floor. Both fields are required together, and `value` is in THOUSANDS from a fixed ladder LinkedIn will not interpolate — dollars (USD/AUD/CAD) step 40…200, pounds (GBP) step 20…100. Off-ladder numbers are refused rather than rounded.
            - object
              - …
            - object
              - …
      - object
        - `id` string: The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new.
        - `signalKey` string, required: The people already on this org’s Pipeline in Nurture, brought back when something happens to them. The ONLY signal that reads your own data instead of searching LinkedIn — so it grows a list with people you already gave up on, not strangers. It must be the ONLY signal on its source, and it never appears twice.
        - `config` object, required: Which news wakes a parked lead, and how long they are left alone afterwards. `triggers`: `changed_jobs` (they moved company — the strongest one, and it re-checks them against the ICP at the new company), `company_news` (the company they work at published a new post), `posted` (they published something you can reply to). At least one. `cooldownDays` defaults to 90 and is never below 7: it is what stops someone who posts every week being written to every week.
          - `triggers` string[], required: Which news wakes a parked lead. At least one. `changed_jobs` — they moved to a new company, the strongest trigger there is; when the list has an ICP, the wake re-checks them against it at the new place. `company_news` — the company they work at published a new post. `posted` — they published something you can reply to.
          - `cooldownDays` integer: How many days a lead is left alone after being woken. Defaults to 90, never below 7. It is what stops someone who posts every week being written to every week — the answer to "how often may we come back", which nothing else in the system decides.
  - `status` 'active' | 'paused': Whether the source keeps feeding the list. `paused` stops it without losing anything — the signals, their places in their feeds, and every member already found all survive, and resuming carries on where it left off. Use this rather than source_delete to stop a list growing for a while. Omitted leaves the current setting; a new source starts `active`.
  - `contactFilter` object: Who this list may add, by how much you have already talked to them — the same rule a campaign enrolls by. Omitted leaves the current setting; a new source adds everyone. Narrow it to stop a list filling up with people already in a conversation.
    - `buckets` string[], required: Which contact-history buckets may enroll: `new` (no conversation either way, and nothing a Kairon campaign sent), `unanswered` (we messaged, no reply), `in_conversation` (they wrote to us). Unique, at least one. All three means enroll everyone. Anyone outside the selected buckets is skipped, not queued, so a narrow set is why a large audience enrolls almost nobody.
    - `scope` 'seat' | 'org', required: Whose prior-contact history is read: `seat` (this campaign's sending seat) or `org` (every LinkedIn seat). Copying a `seat` filter onto a DIFFERENT seat changes what it means — the new seat's history is empty, so people the original filter excluded now enroll.
  - `dailyTarget` integer: How many qualified people this list should gain per day (UTC), 1-1000. A floor, not a cap: every event signal still runs in full first, and the lookalike signal then keeps searching until the day's total reaches this number, the search runs out, 30 people in a row fail the ICP, or 20 companies (or result pages) in a row add nobody (it picks up from there the next day). Only the lookalike fills the gap, so on a source without one this only reports progress. Omitted leaves the current setting; a new source starts at 30.
  - `peoplePerCompany` integer: How many people a walk that goes company by company takes at each company, 1-100. Applies to the lookalike (when the ICP describes companies) and to the recently-raised-funds and new-senior-leadership signals. Higher means fewer companies and less variety, but more of a big employer. Omitted leaves the current setting; a new source starts at 3.
  - `seatId` string: Which workspace seat (a channel account id from seat_list) reads this list's SEAT signals: viewed_my_profile, recently_connected and company_followers, and any signal whose own search is measured from a seat (firstDegreeOnly, postedByMe, postedByFollowed or inYourNetwork in its config, or a networkDistance-style filter in the ICP). Every other signal reads on Kairon's own accounts and needs no seat. Any member may pick any connected seat. Omitted: the list keeps its seat, else takes yours if it can run them; if neither works the call is refused with SOURCE_SEAT_REQUIRED, which lists the seats that can. Refused with SOURCE_SEAT_CANNOT_RUN when the seat lacks what a signal needs (a paid licence for viewed_my_profile, page admin for company_followers, Sales Navigator for a viewer-relative ICP). Ignored when no signal needs a seat.
  - `acceptExtraListCharge` boolean: The workspace's plan includes a number of growing lists (lists whose source is active). Turning a list on past that number costs the plan's price for an extra list, every month, billed to the organization; on a plan that sells none it is refused. A call that turns a list on past the included number is REFUSED unless this is true, and the refusal carries the price: ask the user first, then retry with it. Ignored when the call turns no list on, or the list fits. A list carries at most 3 signals.

## Response `200`

---

[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)
