CLI reference

All 73 commands and their 321 flags, generated from the CLI's command registry — the same list npx cornersight-cli --help prints as JSON. Each maps to one endpoint in the API reference. New to the CLI? Start with the install guide and worked examples.

Every command prints one JSON object on stdout: { ok: true, data }, where data is the API's response verbatim and is the only path to the payload — nothing inside it is copied to the top level. Errors go to stderr as { ok: false, error }. Only the commands marked --no-wait create a background job and poll it to completion; they add jobId beside data. Every other command makes one synchronous request and has no job, no jobId and no --no-wait. Each command below states its own shape, taken from the same contract the CLI executes on, so npx cornersight-cli <command> --help prints exactly this for the version you have installed.

npm currently serves cornersight-cli v4.14.0. Every command below is in it, so anything documented here runs on a fresh install.

Global flags

Accepted by every command, and not repeated in the per-command tables below. --help and --version need no key and make no request, so they work before you have one.

FieldTypeRequiredDescription
--api-keystringyesCornersight API key generated from Settings.
--helpbooleannoPrint JSON help for the CLI or command.
--versionbooleannoPrint the installed CLI version as JSON. Needs no --api-key and makes no request.

Commands

enrich-profile--no-waitCreate a profile enrichment job. With `--save-tracked-profile`, reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).
enrich-company--no-waitCreate a company enrichment job. With `--save-tracked-profile`, reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).
profile-updateChange a tracked person's per-sync credit limit, sync mode, reply capture or raw mode by LinkedIn username, WITHOUT re-tracking them. Re-running `enrich-profile` also sets it, plus an enrichment job; neither re-syncs (`source-sync` does). Reply capture defaults on; `--no-capture-replies` opts out (CLI 4.4.0).
company-updateChange a tracked company page's per-sync credit limit, sync mode, reply capture or raw mode by LinkedIn username, WITHOUT re-tracking it. Re-running `enrich-company` also sets it, plus an enrichment job; neither re-syncs (`source-sync` does). Reply capture defaults on; `--no-capture-replies` opts out (CLI 4.4.0).
get-webhookShow a tracked profile's webhook configuration (URL and delivery flags).
set-webhookConfigure a tracked profile's webhook (partial update: only the flags you pass change).
company-get-webhookShow a tracked company page's webhook configuration.
company-set-webhookConfigure a tracked company page's webhook (partial update).
profile-urnResolve a public LinkedIn handle to the member id the keyword targeting filters take (fromPerson, mentionsPerson). Free: enriches nobody, tracks nobody, creditsCharged is always 0.
profile-posts-readRead a profile's posts and their text WITHOUT tracking it — the untracked counterpart to `profile-posts`, which is a job against a TRACKED source and answers 404 for anyone else. A synchronous GET over any public handle: no source row, no queued sync, no lead written, and NO ENGAGERS collected. ⚠ ONE POST IS ONE CREDIT — this command used to be free and is not. Say how many posts with --posts; without --confirm-spend the call is a 409 that prints as a JSON error and exits 2, having fetched and charged nothing. What bounds it beyond the credit is a tighter rate limit of 10 calls a minute, SHARED with `company-posts-read`.
company-posts-readRead a COMPANY PAGE's posts and their text WITHOUT tracking it — the untracked counterpart to `company-posts`, which is a job against a TRACKED source and answers 404 for anyone else. A synchronous GET over any public handle: no source row, no queued sync, no lead written, and NO ENGAGERS collected. ⚠ ONE POST IS ONE CREDIT — this command used to be free and is not. Say how many posts with --posts; without --confirm-spend the call is a 409 that prints as a JSON error and exits 2, having fetched and charged nothing. What bounds it beyond the credit is a tighter rate limit of 10 calls a minute, SHARED with `profile-posts-read`.
get-icpShow a tracked profile's ICP criteria (the filter rules + match mode behind isIcp / icpOnly).
set-icpSet a tracked profile's ICP criteria (rules and/or match mode). Re-scores existing leads' isIcp.
company-get-icpShow a tracked company page's ICP criteria (filter rules + match mode).
company-set-icpSet a tracked company page's ICP criteria (rules and/or match mode). Re-scores existing leads' isIcp.
sync-statusRead a tracked profile's sync: `state`, `progress` counts, and `isFinal`. Synchronous read: it reports the CURRENT state and returns — nothing is polled for you. THE LIFECYCLE INCLUDES ENRICHMENT AND SO DOES `isFinal`, so `state` stays "enriching" and `isFinal` stays false while captured leads are still being enriched and charged; read `capture` when all you need is that collection finished, and `enrichment` for the pending/completed/failed/skipped breakdown. ⭐ WHY THE RUN ENDED WHERE IT DID IS `capture.stoppedBy`, AND WHAT IT COST IS `capture.creditsSpent`. `credits` means this source's own `--credit-cap-per-sync` was reached and THERE WAS MORE to collect — raise the limit to get it; `exhausted` means the run collected every engagement it found, so a bigger limit changes nothing. `capture_empty` means it collected POSTS and captured NOBODY from them — a CAPTURE failure, never a quiet week: for three days in September 2026 the upstream engager endpoints answered with no rows and every sync reported `completed` with no error. `errorCode` carries the same marker. Before these two, "collected 100 because that was everything" and "collected 100 because 100 was the cap" were the same output. `provider_limit` means the provider stopped serving (~1,100 people on a big post); `capture.coverage` gives declared vs captured. `creditsSpent` is what the run was CHARGED, `leadRowsCaptured` its lead rows; both on a FAILED run too. ⚠ BOTH ARE null UNTIL THE FIRST SYNC FINISHES, and `stoppedBy` is also null on a run that failed (neither word is true of a run that broke) and on one an untrack abandoned (the TOP-LEVEL `stoppedBy` says "untracked" for that). The TOP-LEVEL `stoppedBy` names the same ending, spelling the cap "credit_cap", plus "untracked".
company-sync-statusRead a tracked company page's sync: `state`, `progress` counts, and `isFinal`. Synchronous read: it reports the CURRENT state and returns — nothing is polled for you. THE LIFECYCLE INCLUDES ENRICHMENT AND SO DOES `isFinal`, so `state` stays "enriching" and `isFinal` stays false while captured leads are still being enriched and charged; read `capture` when all you need is that collection finished, and `enrichment` for the pending/completed/failed/skipped breakdown. ⭐ WHY THE RUN ENDED WHERE IT DID IS `capture.stoppedBy`, AND WHAT IT COST IS `capture.creditsSpent`. `credits` means this source's own `--credit-cap-per-sync` was reached and THERE WAS MORE to collect — raise the limit to get it; `exhausted` means the run collected every engagement it found, so a bigger limit changes nothing. `capture_empty` means it collected POSTS and captured NOBODY from them — a CAPTURE failure, never a quiet week: for three days in September 2026 the upstream engager endpoints answered with no rows and every sync reported `completed` with no error. `errorCode` carries the same marker. Before these two, "collected 100 because that was everything" and "collected 100 because 100 was the cap" were the same output. `provider_limit` means the provider stopped serving (~1,100 people on a big post); `capture.coverage` gives declared vs captured. `creditsSpent` is what the run was CHARGED, `leadRowsCaptured` its lead rows; both on a FAILED run too. ⚠ BOTH ARE null UNTIL THE FIRST SYNC FINISHES, and `stoppedBy` is also null on a run that failed (neither word is true of a run that broke) and on one an untrack abandoned (the TOP-LEVEL `stoppedBy` says "untracked" for that). The TOP-LEVEL `stoppedBy` names the same ending, spelling the cap "credit_cap", plus "untracked".
track-postTrack a LinkedIn post and queue its first engagement capture. Reply authors are captured by default; `--no-capture-replies` opts out before lead writes or charges (CLI 4.4.0). Re-tracking an active post can change this setting without another capture; reactivating an untracked post queues a fresh capture.
untrack-profileStop tracking a LinkedIn profile — a SOFT DELETE, and storage and access are separate answers. Nothing is erased: the source is deactivated (status "inactive") and every lead it captured stays in the account. But those leads STOP BEING SERVED — leads-list and engagers-list drop them from the all-sources view and 404 on its id or username, and the source stops being actionable (no push, no webhook or ICP read/write). `sources --include-inactive true` enumerates what you used to track; that lists the source, it does not serve its leads. THE READ IS RE-OPENABLE ON EVERY SURFACE, THIS CLI INCLUDED SINCE 4.1.0: `leads-list --include-inactive true` and `engagers-list --include-inactive true` return what an untracked source kept and make its id resolve instead of 404ing, as GET /api/v1/leads?includeInactive=true and GET /api/v1/engagers?includeInactive=true do (MCP: list_leads / list_engagers with includeInactive). Reading is not acting: the push and the webhook/ICP routes answer 404 under that flag too, and the dashboard has no such switch at all. Re-tracking the same username revives that source and its leads are served again. IT ALSO STOPS WORK ALREADY RUNNING, which is the part that costs money: a sweep in flight is abandoned at the run's next CHECKPOINT — the run asks "am I still tracked?" before each provider call, before each post is harvested and before each chunk of lead rows — so expect it within moments, not at the instant this command returns (a provider call already in flight finishes first, and a failed status read is deliberately not a stop). Leads it had already written are KEPT and the run ends `stoppedBy: "untracked"` on its sync status. QUEUED ENRICHMENT IS WRITTEN OFF AND NOT CHARGED: enrichment lands minutes to hours after capture, so every lead of this source still waiting is marked `skipped` with the reason `source_untracked` and costs no enriching credits. Re-tracking the source returns exactly those leads to pending at the start of its next sync.
track-keywordCreate a keyword search — find posts by up to 10 keywords and capture their engagers as leads, or with `--capture-post-authors` the person who wrote each post (CLI 4.6.0). Name it with `--name`, narrow what the sweep even looks at with the seven provider-side filters (`--author-industry`, `--author-company`, `--author-keyword`, `--from-person`, `--from-company`, `--mentions-person`, `--mentions-company`), filter with your own AI key via `--ai-provider`/`--ai-prompt` (the key itself is set per team per provider in the dashboard, never here), and bound the daily spend with `--credit-cap`. Reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).
untrack-keywordStop a keyword search — a SOFT DELETE: the source is deactivated (status "inactive") and sweeping, with the daily charge, stops. THE ONE KIND WHOSE LEADS STAY READABLE: what it captured is kept AND still served by leads-list and engagers-list, and the search stays pushable — the opposite of untracking a person, company page or post. Find its id again with `sources --include-inactive true`. IT ALSO STOPS WORK ALREADY RUNNING, which is the part that costs money: a sweep in flight is abandoned at the run's next CHECKPOINT — the run asks "am I still tracked?" before each provider call, before each post is harvested and before each chunk of lead rows — so expect it within moments, not at the instant this command returns (a provider call already in flight finishes first, and a failed status read is deliberately not a stop). Leads it had already written are KEPT and the run ends `stoppedBy: "untracked"` on its sync status. QUEUED ENRICHMENT IS WRITTEN OFF AND NOT CHARGED: enrichment lands minutes to hours after capture, so every lead of this source still waiting is marked `skipped` with the reason `source_untracked` and costs no enriching credits. Re-tracking the source returns exactly those leads to pending at the start of its next sync.
keyword-get-webhookShow a keyword search's webhook configuration.
keyword-set-webhookConfigure a keyword search's webhook (partial update).
keyword-get-icpShow a keyword search's ICP criteria (filter rules + match mode).
keyword-set-icpSet a keyword search's ICP criteria (rules and/or match mode). Re-scores existing leads' isIcp.
keyword-sync-statusRead a keyword search's sweep: `state`, `progress` counts, and `isFinal`. Synchronous read: it reports the CURRENT state and returns — nothing is polled for you. THE LIFECYCLE INCLUDES ENRICHMENT AND SO DOES `isFinal`, so `state` stays "enriching" and `isFinal` stays false while captured leads are still being enriched and charged; read `capture` when all you need is that collection finished, and `enrichment` for the pending/completed/failed/skipped breakdown. ⚠ `capture.stoppedBy` IS ALWAYS null FOR A KEYWORD SEARCH: a sweep records its ending on the search, so read `lastRun.stoppedBy` on `sources` (budget, credits, exhausted, error, ai_error, team_cap, lead_cap). `capture.creditsSpent` is what the sweep charged, as `lastRun.creditsSpent`.
keyword-updateChange a live keyword search's settings — its caps, its scope, its AI filter, reply capture, raw mode and provider-side targeting — without re-creating it. PARTIAL: only the flags you give change; everything else is left exactly as it is. Reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).
keyword-estimatePrice a keyword search WITHOUT creating it. Takes the same flags as track-keyword and returns what the next run would be bound by (creditCap, captureMode, datePosted), `estimatedDailyMax` — the most ONE DAY can cost in enriching credits — plus `remainingBalance` and `daysToExhaustAtCap`. Nothing is created, nothing is resumed and nothing is charged, so run it as often as you like. `cornersight track-keyword --dry-run` is the same request under the command you already know.
untrack-postStop tracking a post — a SOFT DELETE: the source is deactivated (status "inactive"), capture stops, and nothing it captured is erased. Its leads STOP BEING SERVED all the same — leads-list and engagers-list drop them from the all-sources view and 404 on its id, and the post stops being actionable. `sources --include-inactive true` still lists it, which says what you used to track rather than serving it again — `leads-list --include-inactive true` and `engagers-list --include-inactive true` read those kept leads back and resolve its id, as GET /api/v1/leads?includeInactive=true and GET /api/v1/engagers?includeInactive=true do (MCP: list_leads / list_engagers with includeInactive), while the dashboard has no such switch at all. Reading is not acting: the push and the webhook and ICP routes answer 404 under that flag too. IT ALSO STOPS WORK ALREADY RUNNING, which is the part that costs money: a sweep in flight is abandoned at the run's next CHECKPOINT — the run asks "am I still tracked?" before each provider call, before each post is harvested and before each chunk of lead rows — so expect it within moments, not at the instant this command returns (a provider call already in flight finishes first, and a failed status read is deliberately not a stop). Leads it had already written are KEPT and the run ends `stoppedBy: "untracked"` on its sync status. QUEUED ENRICHMENT IS WRITTEN OFF AND NOT CHARGED: enrichment lands minutes to hours after capture, so every lead of this source still waiting is marked `skipped` with the reason `source_untracked` and costs no enriching credits. Re-tracking the source returns exactly those leads to pending at the start of its next sync.
untrack-companyStop tracking a LinkedIn company page — a SOFT DELETE, and storage and access are separate answers. Nothing is erased: the source is deactivated (status "inactive") and every lead it captured stays in the account. But those leads STOP BEING SERVED — leads-list and engagers-list drop them from the all-sources view and 404 on its id or username, and the source stops being actionable (no push, no webhook or ICP read/write). `sources --include-inactive true` enumerates what you used to track; that lists the source, it does not serve its leads. THE READ IS RE-OPENABLE ON EVERY SURFACE, THIS CLI INCLUDED SINCE 4.1.0: `leads-list --include-inactive true` and `engagers-list --include-inactive true` return what an untracked source kept and make its id resolve instead of 404ing, as GET /api/v1/leads?includeInactive=true and GET /api/v1/engagers?includeInactive=true do (MCP: list_leads / list_engagers with includeInactive). Reading is not acting: the push and the webhook/ICP routes answer 404 under that flag too, and the dashboard has no such switch at all. Re-tracking the same username revives that source and its leads are served again. IT ALSO STOPS WORK ALREADY RUNNING, which is the part that costs money: a sweep in flight is abandoned at the run's next CHECKPOINT — the run asks "am I still tracked?" before each provider call, before each post is harvested and before each chunk of lead rows — so expect it within moments, not at the instant this command returns (a provider call already in flight finishes first, and a failed status read is deliberately not a stop). Leads it had already written are KEPT and the run ends `stoppedBy: "untracked"` on its sync status. QUEUED ENRICHMENT IS WRITTEN OFF AND NOT CHARGED: enrichment lands minutes to hours after capture, so every lead of this source still waiting is marked `skipped` with the reason `source_untracked` and costs no enriching credits. Re-tracking the source returns exactly those leads to pending at the start of its next sync.
profile-posts--no-waitCreate a job that fetches posts for a tracked LinkedIn profile.
company-posts--no-waitCreate a job that fetches posts for a tracked LinkedIn company page.
post-reactions--no-waitCreate a job that fetches reactions for a tracked LinkedIn post.
post-comments--no-waitAccepts ANY tracked post URN. --size selects 1-50 comments (default 50); --no-include-replies returns top-level comments only. HTTP 500 always falls back. ENGAGER_TRANSIENT_FALLBACK_ENABLED is off by default: 429/502/503/504 and transient network errors fall back only when on; otherwise they surface as errors.
company-post-comments--no-waitAccepts ANY tracked post URN. --size selects 1-50 comments (default 50); --no-include-replies returns top-level comments only. HTTP 500 always falls back. ENGAGER_TRANSIENT_FALLBACK_ENABLED is off by default: 429/502/503/504 and transient network errors fall back only when on; otherwise they surface as errors.
job-statusRead the current status for a Public API job.
creditsGet the team's enriching credit balance (balance, used, limit, remaining, plan, nextReset).
credits-usageGet a dated + by-source breakdown of enriching credits charged (defaults to the current billing period).
keys-listList the team's active API keys (masked) and the per-team quota (used, max, remaining).
keys-createCreate a new named API key. The plaintext key is returned ONCE — store it now, it can't be retrieved again.
keys-revokeRevoke (soft-delete) an API key by id — it stops authenticating immediately. Get the id from keys-list.
leads-listList captured leads without re-sweeping posts. Each lead names its employer in companyName (also company), companyUrl, companyDomain, companyLinkedinUrl, companyDescription, companyIndustry, companyLocation, companyEmployeeCount, companyStaffRange and companyEnrichedAt; unknowns are null. Company fields come from the company record, which Cornersight resolves once per company and caches for every lead at that company. They cost no enriching credits. companyStaffRange is the LinkedIn size bucket and companyEmployeeCount is the reported total, so the two can disagree. companyEnrichedAt is null until the company has been resolved; after that, a null company field means the company record has no value for it. postPostedAt is the post's time; commentPostedAt is the comment's own time, null for Likes and when none could be obtained. --source-kind scopes totals to keyword, post or profile sources, including stopped searches with --include-inactive.
leads-rawList RAW leads — people captured by sources in raw mode (`--no-enrich-leads`) and never enriched: { id, sourceId, linkedinUrl, linkedinUrn, name, action, commentText, post: { url, urn, postedAt }, detectedAt }, newest first. No job title, company or country; `name` is null when capture recorded only an id. Raw leads appear only here, never in leads-list, engagers-list, exports, webhooks or integrations (CLI 4.7.0).
engagers-listList top engagers, one row per person with engagement count. Each engager names its employer in companyName (also company), companyUrl, companyDomain, companyLinkedinUrl, companyDescription, companyIndustry, companyLocation, companyEmployeeCount, companyStaffRange and companyEnrichedAt; unknowns are null. Company fields come from the company record, which Cornersight resolves once per company and caches for every lead at that company. They cost no enriching credits. companyStaffRange is the LinkedIn size bucket and companyEmployeeCount is the reported total, so the two can disagree. companyEnrichedAt is null until the company has been resolved; after that, a null company field means the company record has no value for it.
source-postsRead the posts a POSTS-ONLY watch has seen — the poll beside the `post.detected` push, the way `leads-list` is beside the `lead.detected` webhook. Reading charges NOTHING: the credit was spent when the post was first fetched and these are the same rows again.
kept-postsWhich posts a keyword search's AI filter KEPT, with their permalinks. `sources` gives you lastRun's counts — scanned 25, kept 2 — and this tells you WHICH 2, which is what separates a filter that picked quiet posts from a capture that failed when engagersSeen is 0.
sourcesList the lead sources this team tracks: LinkedIn people, company pages, tracked posts, and keyword searches (each source's id is the profileId for leads-list). Filter with --type person|company|post|keyword. Posts are created with track-post, keyword searches with track-keyword (or the dashboard). A keyword source also carries `filters` — the seven targeting filters (--author-industry etc.) it was created with, present only when at least one is set, so you can read back what track-keyword was given. By default only ACTIVE sources are listed; --include-inactive true adds the untracked ones (status "inactive"), which is how a stopped keyword search's id is found again.
discoverStart a one-off Discover run: find people who post about a topic and get engagement on it. 1 credit per person added.
discover-listList Discover runs, newest first.
discover-getShow a Discover run and its influencers.
discover-deleteDelete a Discover run (soft; its leads are kept).
push-leadsPush already-captured leads for a tracked personal profile to its webhook — historical backfill, ICP-only or all, and the explicit retry for failed deliveries.
company-push-leadsPush already-captured leads for a tracked company page to its webhook — historical backfill, ICP-only or all, and the explicit retry for failed deliveries.
keyword-push-leadsPush already-captured leads for a keyword search to its webhook, by source id — historical backfill, ICP-only or all, and the explicit retry for failed deliveries.
source-push-leadsPush already-captured leads for ANY tracked source to its webhook, by source id — the only push for a tracked post.
push-statusShow the progress of a lead push: how many of its leads are pending, sending, delivered, failed or held, and whether it has finished.
source-sync-statusCheck ANY tracked source's background capture sync by source id — stage, progress counts, and whether it has finished. The only way to follow a tracked post's lifecycle.
source-syncSync a person, company page or post now, by source id; charged like any sync.
source-get-webhookShow ANY tracked source's webhook configuration by source id — URL, ICP-only, auto-send and sync-events.
source-set-webhookConfigure ANY tracked source's webhook by source id (partial update).
source-get-icpShow ANY tracked source's ICP criteria by source id (filter rules + match mode).
source-set-icpSet ANY tracked source's ICP criteria by source id (rules and/or match mode). Re-scores existing leads' isIcp.
source-get-heyreachShow any source's HeyReach auto-push: the campaign its new leads go to, by source id.
source-set-heyreachSend any source's new leads to a HeyReach campaign automatically, by source id.
agentShow the Engagement Agent: profile, daily limit and split, status, webhook, people search and sources.
agent-setCreate or update the Engagement Agent's profile, webhook and HeyReach auto-push. Partial; starts nothing, charges nothing.
agent-startStart or restart the Engagement Agent: a daily search per topic and a one-off people search.
agent-stopStop the Engagement Agent: untrack every source it set up; back to draft. Leads are kept.
agent-leadsList Agent Leads: one row per person, ranked by ICP % then signal, as the dashboard shows them.
agent-pushSend Agent Leads to the agent's webhook now: those passing its filters, or --lead-ids.
heyreachShow the HeyReach connection: whether it is connected, whether HeyReach rejected the key, and every auto-push.
heyreach-campaignsList the team's HeyReach campaigns that leads can join, with their LinkedIn senders.
heyreach-sendAdd leads, or influencers found by Discover, to a HeyReach campaign.

Flags, command by command

What each command accepts, beyond the global flags above.

enrich-profile

POST/api/v1/enrich/profile

Create a profile enrichment job. With `--save-tracked-profile`, reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).

FieldTypeRequiredDescription
--usernamestringyesLinkedIn profile username, not a full URL.
--save-tracked-profilebooleannoCreate or update the tracked profile before enrichment.default: false
--capture-repliesbooleannoCapture reply authors as leads (default true); false skips replies before lead writes and credits.
--enrich-leadsbooleannoEnrich this source's leads (default true). --no-enrich-leads (or --enrich-leads false) keeps them RAW: captured and charged exactly as before — one credit per NEW person per source, repeats free — but never enriched (no job title, company or country), and read only with `leads-raw`, never leads-list, engagers-list, exports, webhooks or integrations. Needs no --confirm-spend. Applies to leads captured after the change; omitted leaves the stored setting alone. CLI 4.7.0.
--credit-cap-per-syncintegernoCapture cap counts lead rows; billing charges once per new person per source. The most enriching credits ONE SYNC of this tracked source may spend — THE CAP COUNTS LEAD ROWS. PER SYNC AND NOT A LIFETIME TOTAL: the source re-syncs on its own about every 24 hours and this bounds EACH of those runs, so 500 here authorises up to 500 credits every sync rather than 500 once. NEEDS --save-tracked-profile: without it this call creates no tracked source for the limit to sit on, and the CLI refuses LOCALLY rather than spending a round trip on the API's 400. OMITTED LEAVES THE STORED LIMIT EXACTLY AS IT IS — on a source you already track as well as on a new one — so re-running this never silently removes a cap somebody set. Deliberately unbounded above: a source with no limit is uncapped, which is what every source created before this flag has been. CHANGING IT LATER without re-syncing is `profile-update --username <handle> --credit-cap-per-sync <n>`. Read it back as `creditCapPerSync` on `sources`; a sync that ENDED on it reports `stoppedBy: "credit_cap"` on `source-sync-status`. NOT the keyword search field of the same old name: `track-keyword --credit-cap` is a search's per-RUN cap, a different number on a different source kind. PER SYNC, EVERY SYNC: it bounds each of those syncs, not the first pull only. NO ESTIMATE, NO CONFIRMATION GATE AND NO TEAM CEILING stand behind it — nothing prices a sync before you set the cap, raising or lowering it never needs --confirm-spend, and the team’s daily keyword ceiling does not count a credit of it. A keyword search’s --credit-cap HAS all three: it is the daily bound on a RECURRING SWEEP, `keyword-estimate` prices it, --confirm-spend gates a create or a raise, and the team’s `dailyCeiling` stops it — and that ceiling COUNTS KEYWORD SPEND ONLY, so no number of profile syncs can ever reach it.
--modestringnoWHAT THIS TRACKED SOURCE DOES ON EACH SYNC. `engagers` (the default, and what every tracked source has always done): sweep the posts and fan out across the reactions and comments on each one, writing a lead per engager and charging ONE CREDIT PER NEW PERSON PER SOURCE; repeat engagements are free — one post with 300 distinct new people can cost up to 300 credits. `posts-only`: fetch this source's NEW POSTS newest first and stop — no engagers, no leads, no enrichment — charging ONE CREDIT PER NEW POST, up to --posts-per-sync a day, and never twice for the same post. ASK WHICH IS WANTED BEFORE CREATING ANYTHING: “watch what this person posts” and “find me the people who engage with them” are different products at very different prices. `posts-only` NEEDS --posts-per-sync AND --confirm-spend. NEEDS --save-tracked-profile: without it this call creates no source for a mode to apply to. Sending it for a source you ALREADY track CHANGES its mode, which is how it is edited — and switching an existing posts-only source TO engagers is a spend increase that needs --confirm-spend, while switching the other way never does. Read it back as `mode` on `sources`.
--posts-per-syncintegernoThe most posts ONE SYNC of a posts-only source may fetch, 1-60 — and, because one post is one credit, the most it can cost in a day: the N in “up to N credits a day”. REQUIRED with `--mode posts-only` and refused without it, because that sentence is what somebody confirms and there is no N to put in it otherwise. It never backfills: a sync buys only posts newer than the newest it holds (the first sync, up to N of the newest), so a quiet day costs nothing. --credit-cap-per-sync applies as well and the TIGHTER of the two binds a run. Read it back as `postsPerSync` on `sources`.
--first-sync-postsintegernoFIRST SYNC ONLY: how many of the latest posts a new source's first sync collects, 1-50 (default 15); later syncs check the 4 newest. Each post's engagers are charged as usual. Needs --save-tracked-profile; refused with --mode posts-only.
--first-sync-daysintegernoFIRST SYNC ONLY: only posts from this many days back, 1-90, at most 50 (or --first-sync-posts); undated posts are left out. Needs --save-tracked-profile; refused with --mode posts-only.
--confirm-spendbooleannoAuthorises the RECURRING charge a posts-only source creates. Without it the call is refused 409 `spend_confirmation_required` and NOTHING is created; the CLI prints that refusal as a JSON error on stderr and exits 2, with `estimatedDailyMax`, `daysToExhaustAtCap` and `remainingBalance` in it. READ THE DAILY FIGURE TO WHOEVER IS PAYING AND GET A YES, then re-run the identical command with --confirm-spend. This is a STANDING charge until the source is untracked, not a one-off. It is also what authorises switching an existing posts-only source back to the engagers sweep.
--no-waitbooleannoReturn immediately instead of polling the job to completion. The accepted job id is at data.jobId — read the result later with `job-status --job-id <data.jobId>`.default: false

Writing true, false and neither

True: --save-tracked-profile, --save-tracked-profile true, --save-tracked-profile=true. False: --no-save-tracked-profile, --save-tracked-profile false, --save-tracked-profile=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-save-tracked-profile, --no-capture-replies, --no-enrich-leads, --no-confirm-spend.

What it prints

{ ok, jobId, data } — the JOB-STATUS response verbatim: data.status is the final state and data.result the payload. jobId sits beside data because the job-status response does not carry it. Read the payload at data.result. Polls GET /api/v1/jobs/{jobId}/status until the job is completed or failed, with no timeout. With --no-wait it prints { ok, data } instead and polls nothing — the accepted id is at data.jobId.

enrich-company

POST/api/v1/enrich/company

Create a company enrichment job. With `--save-tracked-profile`, reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).

FieldTypeRequiredDescription
--usernamestringyesLinkedIn company username, not a full URL.
--save-tracked-profilebooleannoCreate or update the tracked company before enrichment.default: false
--capture-repliesbooleannoCapture reply authors as leads (default true); false skips replies before lead writes and credits.
--enrich-leadsbooleannoEnrich this source's leads (default true). --no-enrich-leads (or --enrich-leads false) keeps them RAW: captured and charged exactly as before — one credit per NEW person per source, repeats free — but never enriched (no job title, company or country), and read only with `leads-raw`, never leads-list, engagers-list, exports, webhooks or integrations. Needs no --confirm-spend. Applies to leads captured after the change; omitted leaves the stored setting alone. CLI 4.7.0.
--credit-cap-per-syncintegernoCapture cap counts lead rows; billing charges once per new person per source. The most enriching credits ONE SYNC of this tracked source may spend — THE CAP COUNTS LEAD ROWS. PER SYNC AND NOT A LIFETIME TOTAL: the source re-syncs on its own about every 24 hours and this bounds EACH of those runs, so 500 here authorises up to 500 credits every sync rather than 500 once. NEEDS --save-tracked-profile: without it this call creates no tracked source for the limit to sit on, and the CLI refuses LOCALLY rather than spending a round trip on the API's 400. OMITTED LEAVES THE STORED LIMIT EXACTLY AS IT IS — on a source you already track as well as on a new one — so re-running this never silently removes a cap somebody set. Deliberately unbounded above: a source with no limit is uncapped, which is what every source created before this flag has been. CHANGING IT LATER without re-syncing is `company-update --username <handle> --credit-cap-per-sync <n>`. Read it back as `creditCapPerSync` on `sources`; a sync that ENDED on it reports `stoppedBy: "credit_cap"` on `source-sync-status`. NOT the keyword search field of the same old name: `track-keyword --credit-cap` is a search's per-RUN cap, a different number on a different source kind. PER SYNC, EVERY SYNC: it bounds each of those syncs, not the first pull only. NO ESTIMATE, NO CONFIRMATION GATE AND NO TEAM CEILING stand behind it — nothing prices a sync before you set the cap, raising or lowering it never needs --confirm-spend, and the team’s daily keyword ceiling does not count a credit of it. A keyword search’s --credit-cap HAS all three: it is the daily bound on a RECURRING SWEEP, `keyword-estimate` prices it, --confirm-spend gates a create or a raise, and the team’s `dailyCeiling` stops it — and that ceiling COUNTS KEYWORD SPEND ONLY, so no number of profile syncs can ever reach it.
--modestringnoWHAT THIS TRACKED SOURCE DOES ON EACH SYNC. `engagers` (the default, and what every tracked source has always done): sweep the posts and fan out across the reactions and comments on each one, writing a lead per engager and charging ONE CREDIT PER NEW PERSON PER SOURCE; repeat engagements are free — one post with 300 distinct new people can cost up to 300 credits. `posts-only`: fetch this source's NEW POSTS newest first and stop — no engagers, no leads, no enrichment — charging ONE CREDIT PER NEW POST, up to --posts-per-sync a day, and never twice for the same post. ASK WHICH IS WANTED BEFORE CREATING ANYTHING: “watch what this person posts” and “find me the people who engage with them” are different products at very different prices. `posts-only` NEEDS --posts-per-sync AND --confirm-spend. NEEDS --save-tracked-profile: without it this call creates no source for a mode to apply to. Sending it for a source you ALREADY track CHANGES its mode, which is how it is edited — and switching an existing posts-only source TO engagers is a spend increase that needs --confirm-spend, while switching the other way never does. Read it back as `mode` on `sources`.
--posts-per-syncintegernoThe most posts ONE SYNC of a posts-only source may fetch, 1-60 — and, because one post is one credit, the most it can cost in a day: the N in “up to N credits a day”. REQUIRED with `--mode posts-only` and refused without it, because that sentence is what somebody confirms and there is no N to put in it otherwise. It never backfills: a sync buys only posts newer than the newest it holds (the first sync, up to N of the newest), so a quiet day costs nothing. --credit-cap-per-sync applies as well and the TIGHTER of the two binds a run. Read it back as `postsPerSync` on `sources`.
--first-sync-postsintegernoFIRST SYNC ONLY: how many of the latest posts a new source's first sync collects, 1-50 (default 15); later syncs check the 4 newest. Each post's engagers are charged as usual. Needs --save-tracked-profile; refused with --mode posts-only.
--first-sync-daysintegernoFIRST SYNC ONLY: only posts from this many days back, 1-90, at most 50 (or --first-sync-posts); undated posts are left out. Needs --save-tracked-profile; refused with --mode posts-only.
--confirm-spendbooleannoAuthorises the RECURRING charge a posts-only source creates. Without it the call is refused 409 `spend_confirmation_required` and NOTHING is created; the CLI prints that refusal as a JSON error on stderr and exits 2, with `estimatedDailyMax`, `daysToExhaustAtCap` and `remainingBalance` in it. READ THE DAILY FIGURE TO WHOEVER IS PAYING AND GET A YES, then re-run the identical command with --confirm-spend. This is a STANDING charge until the source is untracked, not a one-off. It is also what authorises switching an existing posts-only source back to the engagers sweep.
--no-waitbooleannoReturn immediately instead of polling the job to completion. The accepted job id is at data.jobId — read the result later with `job-status --job-id <data.jobId>`.default: false

Writing true, false and neither

True: --save-tracked-profile, --save-tracked-profile true, --save-tracked-profile=true. False: --no-save-tracked-profile, --save-tracked-profile false, --save-tracked-profile=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-save-tracked-profile, --no-capture-replies, --no-enrich-leads, --no-confirm-spend.

What it prints

{ ok, jobId, data } — the JOB-STATUS response verbatim: data.status is the final state and data.result the payload. jobId sits beside data because the job-status response does not carry it. Read the payload at data.result. Polls GET /api/v1/jobs/{jobId}/status until the job is completed or failed, with no timeout. With --no-wait it prints { ok, data } instead and polls nothing — the accepted id is at data.jobId.

profile-update

PATCH/api/v1/profile/{username}

Change a tracked person's per-sync credit limit, sync mode, reply capture or raw mode by LinkedIn username, WITHOUT re-tracking them. Re-running `enrich-profile` also sets it, plus an enrichment job; neither re-syncs (`source-sync` does). Reply capture defaults on; `--no-capture-replies` opts out (CLI 4.4.0).

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn person username, not a full URL and not a source id.
--capture-repliesbooleannoCapture reply authors as leads (default true); false skips replies before lead writes and credits.
--enrich-leadsbooleannoEnrich this source's leads (default true). --no-enrich-leads (or --enrich-leads false) keeps them RAW: captured and charged exactly as before — one credit per NEW person per source, repeats free — but never enriched (no job title, company or country), and read only with `leads-raw`, never leads-list, engagers-list, exports, webhooks or integrations. Needs no --confirm-spend. Applies to leads captured after the change; omitted leaves the stored setting alone. CLI 4.7.0.
--modestringnoSwitch what each sync of this source does: `engagers` (capture the people who engaged with its posts as leads, one credit per NEW person per source) or `posts-only` (fetch its new posts only, one credit per new post up to --posts-per-sync, no leads). Omit to leave the mode alone. Switching posts-only to engagers can raise the cost, so the API refuses it 409 `spend_confirmation_required` and changes nothing until you re-run with --confirm-spend; switching to posts-only never needs it. Binds from the next sync. CLI 4.12.0.
--posts-per-syncintegernoWith `--mode posts-only` only, and required with it: the most posts ONE SYNC may fetch, 1-60, which is also the most the source can cost in a day. The API refuses it with `--mode engagers` or without --mode. CLI 4.12.0.
--confirm-spendbooleannoAuthorises switching a posts-only source to the engagers sweep, the one change here that can raise what it costs. Without it that switch is refused 409 `spend_confirmation_required` (printed on stderr, exit 2) and NOTHING is changed. Get a yes from whoever pays first. Not a setting on its own. CLI 4.12.0.
--credit-cap-per-syncintegernoCapture cap counts lead rows; billing charges once per new person per source. The most enriching credits ONE SYNC of this source may spend — THE CAP COUNTS LEAD ROWS. PER SYNC AND NOT A LIFETIME TOTAL: the source re-syncs about every 24 hours and this bounds EACH of those runs. PASS IT WITH NO VALUE TO REMOVE THE LIMIT — that sends `creditCapPerSync: null`, the API’s word for “no limit”, which no integer flag can spell and which `0` is refused for. Raises and lowers alike need no confirmation, because unlike a keyword search this limit binds one sync rather than authorising a recurring daily charge. PER SYNC, EVERY SYNC: it bounds each of those syncs, not the first pull only. NO ESTIMATE, NO CONFIRMATION GATE AND NO TEAM CEILING stand behind it — nothing prices a sync before you set the cap, raising or lowering it never needs --confirm-spend, and the team’s daily keyword ceiling does not count a credit of it. A keyword search’s --credit-cap HAS all three: it is the daily bound on a RECURRING SWEEP, `keyword-estimate` prices it, --confirm-spend gates a create or a raise, and the team’s `dailyCeiling` stops it — and that ceiling COUNTS KEYWORD SPEND ONLY, so no number of profile syncs can ever reach it. Deliberately unbounded above: what stops a re-syncing source is your team’s monthly balance. Omitting the flag entirely changes nothing, which is an error rather than a no-op.

Writing true, false and neither

True: --capture-replies, --capture-replies true, --capture-replies=true. False: --no-capture-replies, --capture-replies false, --capture-replies=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-capture-replies, --no-enrich-leads, --no-confirm-spend.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • IT CHANGES THE LIMIT AND NOTHING ELSE. No sync is queued, no lead is enriched, nothing is charged — which is the difference between this and re-running `enrich-profile --save-tracked-profile --credit-cap-per-sync N`, whose effect on the limit is identical; neither re-syncs. A call naming no change at all is an error rather than a silent no-op.
  • ADDRESSED BY THE SAME USERNAME EVERY OTHER person COMMAND TAKES — the handle, not a source id and not a URL. That is deliberate: `sync-status`, `get-webhook`, `set-icp`, `push-leads` and `untrack-profile` are all keyed that way, so the identifier you already have is the identifier this needs.
  • --credit-cap-per-sync RAISES OR LOWERS; PASSING IT WITH NO VALUE REMOVES THE LIMIT. The API’s word for “no limit” is an explicit JSON null, which no integer flag can express — so the bare flag is that word. `--credit-cap-per-sync 0` is a 400 on every surface, because a source that syncs and writes nothing is a PAUSED source and `status` already says that.
  • AN EDIT BINDS FROM THE NEXT SYNC, NEVER THE ONE ALREADY RUNNING. A sync reads the source’s limit when it starts and holds it for the whole run, so a cap lowered mid-sweep does not cut the sweep short and a run that began uncapped finishes uncapped. Read `stoppedBy` on `sync-status` to see whether the run you are looking at actually ended at a limit.
  • NOT A KEYWORD SEARCH’S CAP. `creditCap` on a keyword search is its per-RUN limit, it lives on a different table, and `keyword-update --id <id> --credit-cap N` owns it. The two used to share a name; they no longer do, and this command reaches only a person.
  • --mode SWITCHES WHAT EACH SYNC DOES. `--mode posts-only --posts-per-sync N` makes the source a posts-only watch (new posts only, one credit per new post, at most N a day, no leads); `--mode engagers` turns it back into the engagers sweep. Only that second switch can raise the cost: without --confirm-spend it is refused 409 `spend_confirmation_required` and nothing is changed. Read the cost to whoever pays and re-run with --confirm-spend only after a yes.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person is neither readable nor actionable. Re-track it first if you meant to bring it back.

company-update

PATCH/api/v1/company/{username}

Change a tracked company page's per-sync credit limit, sync mode, reply capture or raw mode by LinkedIn username, WITHOUT re-tracking it. Re-running `enrich-company` also sets it, plus an enrichment job; neither re-syncs (`source-sync` does). Reply capture defaults on; `--no-capture-replies` opts out (CLI 4.4.0).

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company page username, not a full URL and not a source id.
--capture-repliesbooleannoCapture reply authors as leads (default true); false skips replies before lead writes and credits.
--enrich-leadsbooleannoEnrich this source's leads (default true). --no-enrich-leads (or --enrich-leads false) keeps them RAW: captured and charged exactly as before — one credit per NEW person per source, repeats free — but never enriched (no job title, company or country), and read only with `leads-raw`, never leads-list, engagers-list, exports, webhooks or integrations. Needs no --confirm-spend. Applies to leads captured after the change; omitted leaves the stored setting alone. CLI 4.7.0.
--modestringnoSwitch what each sync of this source does: `engagers` (capture the people who engaged with its posts as leads, one credit per NEW person per source) or `posts-only` (fetch its new posts only, one credit per new post up to --posts-per-sync, no leads). Omit to leave the mode alone. Switching posts-only to engagers can raise the cost, so the API refuses it 409 `spend_confirmation_required` and changes nothing until you re-run with --confirm-spend; switching to posts-only never needs it. Binds from the next sync. CLI 4.12.0.
--posts-per-syncintegernoWith `--mode posts-only` only, and required with it: the most posts ONE SYNC may fetch, 1-60, which is also the most the source can cost in a day. The API refuses it with `--mode engagers` or without --mode. CLI 4.12.0.
--confirm-spendbooleannoAuthorises switching a posts-only source to the engagers sweep, the one change here that can raise what it costs. Without it that switch is refused 409 `spend_confirmation_required` (printed on stderr, exit 2) and NOTHING is changed. Get a yes from whoever pays first. Not a setting on its own. CLI 4.12.0.
--credit-cap-per-syncintegernoCapture cap counts lead rows; billing charges once per new person per source. The most enriching credits ONE SYNC of this source may spend — THE CAP COUNTS LEAD ROWS. PER SYNC AND NOT A LIFETIME TOTAL: the source re-syncs about every 24 hours and this bounds EACH of those runs. PASS IT WITH NO VALUE TO REMOVE THE LIMIT — that sends `creditCapPerSync: null`, the API’s word for “no limit”, which no integer flag can spell and which `0` is refused for. Raises and lowers alike need no confirmation, because unlike a keyword search this limit binds one sync rather than authorising a recurring daily charge. PER SYNC, EVERY SYNC: it bounds each of those syncs, not the first pull only. NO ESTIMATE, NO CONFIRMATION GATE AND NO TEAM CEILING stand behind it — nothing prices a sync before you set the cap, raising or lowering it never needs --confirm-spend, and the team’s daily keyword ceiling does not count a credit of it. A keyword search’s --credit-cap HAS all three: it is the daily bound on a RECURRING SWEEP, `keyword-estimate` prices it, --confirm-spend gates a create or a raise, and the team’s `dailyCeiling` stops it — and that ceiling COUNTS KEYWORD SPEND ONLY, so no number of profile syncs can ever reach it. Deliberately unbounded above: what stops a re-syncing source is your team’s monthly balance. Omitting the flag entirely changes nothing, which is an error rather than a no-op.

Writing true, false and neither

True: --capture-replies, --capture-replies true, --capture-replies=true. False: --no-capture-replies, --capture-replies false, --capture-replies=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-capture-replies, --no-enrich-leads, --no-confirm-spend.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • IT CHANGES THE LIMIT AND NOTHING ELSE. No sync is queued, no lead is enriched, nothing is charged — which is the difference between this and re-running `enrich-company --save-tracked-profile --credit-cap-per-sync N`, whose effect on the limit is identical; neither re-syncs. A call naming no change at all is an error rather than a silent no-op.
  • ADDRESSED BY THE SAME USERNAME EVERY OTHER company page COMMAND TAKES — the handle, not a source id and not a URL. That is deliberate: `sync-status`, `get-webhook`, `set-icp`, `push-leads` and `untrack-company` are all keyed that way, so the identifier you already have is the identifier this needs.
  • --credit-cap-per-sync RAISES OR LOWERS; PASSING IT WITH NO VALUE REMOVES THE LIMIT. The API’s word for “no limit” is an explicit JSON null, which no integer flag can express — so the bare flag is that word. `--credit-cap-per-sync 0` is a 400 on every surface, because a source that syncs and writes nothing is a PAUSED source and `status` already says that.
  • AN EDIT BINDS FROM THE NEXT SYNC, NEVER THE ONE ALREADY RUNNING. A sync reads the source’s limit when it starts and holds it for the whole run, so a cap lowered mid-sweep does not cut the sweep short and a run that began uncapped finishes uncapped. Read `stoppedBy` on `company-sync-status` to see whether the run you are looking at actually ended at a limit.
  • NOT A KEYWORD SEARCH’S CAP. `creditCap` on a keyword search is its per-RUN limit, it lives on a different table, and `keyword-update --id <id> --credit-cap N` owns it. The two used to share a name; they no longer do, and this command reaches only a company page.
  • --mode SWITCHES WHAT EACH SYNC DOES. `--mode posts-only --posts-per-sync N` makes the source a posts-only watch (new posts only, one credit per new post, at most N a day, no leads); `--mode engagers` turns it back into the engagers sweep. Only that second switch can raise the cost: without --confirm-spend it is refused 409 `spend_confirmation_required` and nothing is changed. Read the cost to whoever pays and re-run with --confirm-spend only after a yes.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated company page is neither readable nor actionable. Re-track it first if you meant to bring it back.

get-webhook

GET/api/v1/profile/{username}/webhook

Show a tracked profile's webhook configuration (URL and delivery flags).

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn profile username, not a full URL.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

set-webhook

PUT/api/v1/profile/{username}/webhook

Configure a tracked profile's webhook (partial update: only the flags you pass change).

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn profile username, not a full URL.
--webhook-urlstringnoPublic https URL to POST leads to. Pass an empty string to clear it.
--icp-onlybooleannoOnly deliver leads that match the source's ICP filter.
--auto-sendbooleannoAuto-deliver new leads (true) or deliver only on explicit push (false).
--sync-eventsbooleannoAlso send sync.completed/sync.failed callbacks (needs a webhook URL).

Writing true, false and neither

True: --icp-only, --icp-only true, --icp-only=true. False: --no-icp-only, --icp-only false, --icp-only=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-icp-only, --no-auto-send, --no-sync-events.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

company-get-webhook

GET/api/v1/company/{username}/webhook

Show a tracked company page's webhook configuration.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company username, not a full URL.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

company-set-webhook

PUT/api/v1/company/{username}/webhook

Configure a tracked company page's webhook (partial update).

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company username, not a full URL.
--webhook-urlstringnoPublic https URL to POST leads to. Pass an empty string to clear it.
--icp-onlybooleannoOnly deliver leads that match the source's ICP filter.
--auto-sendbooleannoAuto-deliver new leads (true) or deliver only on explicit push (false).
--sync-eventsbooleannoAlso send sync.completed/sync.failed callbacks (needs a webhook URL).

Writing true, false and neither

True: --icp-only, --icp-only true, --icp-only=true. False: --no-icp-only, --icp-only false, --icp-only=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-icp-only, --no-auto-send, --no-sync-events.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

profile-urn

GET/api/v1/profile/{username}/urn

Resolve a public LinkedIn handle to the member id the keyword targeting filters take (fromPerson, mentionsPerson). Free: enriches nobody, tracks nobody, creditsCharged is always 0.

FieldTypeRequiredDescription
--usernamestringyesPublic LinkedIn handle from a profile URL, not a full URL, e.g. demo-profile. Need not be tracked or enriched.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

profile-posts-read

GET/api/v1/profile/{username}/posts

Read a profile's posts and their text WITHOUT tracking it — the untracked counterpart to `profile-posts`, which is a job against a TRACKED source and answers 404 for anyone else. A synchronous GET over any public handle: no source row, no queued sync, no lead written, and NO ENGAGERS collected. ⚠ ONE POST IS ONE CREDIT — this command used to be free and is not. Say how many posts with --posts; without --confirm-spend the call is a 409 that prints as a JSON error and exits 2, having fetched and charged nothing. What bounds it beyond the credit is a tighter rate limit of 10 calls a minute, SHARED with `company-posts-read`.

FieldTypeRequiredDescription
--usernamestringyesPublic LinkedIn handle from a profile URL, not a full URL, e.g. demo-profile. NEED NOT BE TRACKED — that is the point of this command.
--postsintegeryesHow many posts to fetch, 1-60, newest first. ONE POST IS ONE CREDIT, so this is also the most this call can cost. REQUIRED, with no default: a default here would be a spend you never typed. You are charged for the posts ACTUALLY RETURNED, so a profile with fewer costs fewer.
--confirm-spendbooleannoAuthorises the charge. WITHOUT IT THE CALL IS A 409 and nothing is fetched or charged — the CLI prints that refusal as a JSON error on stderr and EXITS 2, with the code and the estimate in it. Read the cost to whoever is paying, get a yes, then re-run the identical command with --confirm-spend.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • ONE POST IS ONE CREDIT, AND THIS COMMAND USED TO BE FREE. Verified 20 September: 60 posts pulled for a team whose credits used did not move. That is over. You say how many posts you want with --posts and the call charges for the posts it returns — `creditsCharged` in the body is what you were actually billed, and it EQUALS the number of posts returned, so a profile holding twelve posts costs twelve however many you asked for.
  • ASK TWO THINGS BEFORE FETCHING ANYTHING: HOW MANY POSTS (1-60, and one post is one credit), and is this a ONE-OFF READ or a DAILY WATCH? A one-off read is this command with --posts and --confirm-spend. A daily watch is a posts-only tracked source (`enrich-profile --save-tracked-profile --mode posts-only --posts-per-sync <n> --confirm-spend`), which costs up to that many credits EVERY DAY until it is untracked.
  • WITHOUT --confirm-spend THE CALL IS REFUSED AND NOTHING IS FETCHED. The API answers 409 `spend_confirmation_required` carrying `estimatedCredits` (equal to --posts) and `remainingBalance`; the CLI prints that body as a JSON error on stderr and exits 2. Nothing was charged and no provider call was made — the round trip IS the consent. Re-run the identical command with --confirm-spend to fetch and charge.
  • A BALANCE BELOW WHAT YOU ASKED FOR IS A 402, NOT A SHORT ANSWER. The refusal names both numbers and nothing is fetched. A partial set is deliberately not offered: serving nine posts of a confirmed twenty would be a different call from the one you authorised. Ask for fewer, or top the team up.
  • THERE IS NO PAGING ANY MORE. `--limit` and `--pagination-token` are gone and the API refuses them with a 400 naming --posts, because “fetch 20” and “fetch 15 then 5 more” must not be two prices for one answer. The 15-per-page walk still happens — internally, and `maxDepth` in the body is still how far into a history this read goes.
  • THIS IS THE UNTRACKED READ; `profile-posts` IS THE TRACKED ONE. They are different commands against different endpoints: `profile-posts` is a job against POST /api/v1/profile/posts, it answers 404 “Profile not tracked” for anyone you have not tracked, and it PERSISTS every post it walks against that tracked source. This one is a synchronous GET, takes any public handle, and writes nothing anywhere.
  • IT COLLECTS NO ENGAGERS, AND THAT IS THE EXPENSIVE HALF YOU ARE SKIPPING. A tracked sweep finds posts through this same provider call and then fans out across the reactions and comments on each one — that fan-out is where a sweep’s cost and all of its LEADS come from. This returns the posts and their text and stops, and `captured: false` says so in the body: paying for a read does not make it a capture. If you wanted the people who engaged, you wanted `enrich-profile --save-tracked-profile` (which tracks and sweeps and charges per LEAD).
  • A RATE LIMIT STILL BOUNDS IT, AND IT IS NOW THE SECONDARY GUARD RATHER THAN THE ONLY ONE. Both posts-read commands share ONE per-team budget of 10 CALLS A MINUTE, well below the 120/min ordinary reads get. A 429 HERE DOES NOT MEAN YOU ARE OUT OF CREDITS — that is a 402 with its own code — it means the two commands together were called too fast. Wait for the window the `Retry-After` header names instead of retrying in a loop.
  • AN EMPTY `posts` ARRAY IS A REAL ANSWER, NOT A FAILURE, AND IT COSTS NOTHING. It means that profile has no posts we can read — a private profile, one that only reshares, or one that has never posted. It is deliberately not a 404, because a 404 would be indistinguishable from a handle you typed wrong. A provider that would not answer at all is a 502 carrying its own code, and that charges nothing either.
  • `text` IS NULL, NEVER MISSING, when the provider served a post without any — normal for an image or video post. Every field of a post is present in every row for exactly this reason: read `null`, do not test for the key.

company-posts-read

GET/api/v1/company/{username}/posts

Read a COMPANY PAGE's posts and their text WITHOUT tracking it — the untracked counterpart to `company-posts`, which is a job against a TRACKED source and answers 404 for anyone else. A synchronous GET over any public handle: no source row, no queued sync, no lead written, and NO ENGAGERS collected. ⚠ ONE POST IS ONE CREDIT — this command used to be free and is not. Say how many posts with --posts; without --confirm-spend the call is a 409 that prints as a JSON error and exits 2, having fetched and charged nothing. What bounds it beyond the credit is a tighter rate limit of 10 calls a minute, SHARED with `profile-posts-read`.

FieldTypeRequiredDescription
--usernamestringyesThe company page's slug from a company page URL, e.g. instantlyapp from linkedin.com/company/instantlyapp. A full company URL is accepted and unwrapped. NEED NOT BE TRACKED — that is the point of this command.
--postsintegeryesHow many posts to fetch, 1-60, newest first. ONE POST IS ONE CREDIT, so this is also the most this call can cost. REQUIRED, with no default: a default here would be a spend you never typed. You are charged for the posts ACTUALLY RETURNED, so a page with fewer costs fewer.
--confirm-spendbooleannoAuthorises the charge. WITHOUT IT THE CALL IS A 409 and nothing is fetched or charged — the CLI prints that refusal as a JSON error on stderr and EXITS 2, with the code and the estimate in it. Read the cost to whoever is paying, get a yes, then re-run the identical command with --confirm-spend.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • ONE POST IS ONE CREDIT, AND THIS COMMAND USED TO BE FREE. Verified 20 September: 60 posts pulled for a team whose credits used did not move. That is over. You say how many posts you want with --posts and the call charges for the posts it returns — `creditsCharged` in the body is what you were actually billed, and it EQUALS the number of posts returned, so a page holding twelve posts costs twelve however many you asked for.
  • ASK TWO THINGS BEFORE FETCHING ANYTHING: HOW MANY POSTS (1-60, and one post is one credit), and is this a ONE-OFF READ or a DAILY WATCH? A one-off read is this command with --posts and --confirm-spend. A daily watch is a posts-only tracked source (`enrich-profile --save-tracked-profile --mode posts-only --posts-per-sync <n> --confirm-spend`), which costs up to that many credits EVERY DAY until it is untracked.
  • WITHOUT --confirm-spend THE CALL IS REFUSED AND NOTHING IS FETCHED. The API answers 409 `spend_confirmation_required` carrying `estimatedCredits` (equal to --posts) and `remainingBalance`; the CLI prints that body as a JSON error on stderr and exits 2. Nothing was charged and no provider call was made — the round trip IS the consent. Re-run the identical command with --confirm-spend to fetch and charge.
  • A BALANCE BELOW WHAT YOU ASKED FOR IS A 402, NOT A SHORT ANSWER. The refusal names both numbers and nothing is fetched. A partial set is deliberately not offered: serving nine posts of a confirmed twenty would be a different call from the one you authorised. Ask for fewer, or top the team up.
  • THERE IS NO PAGING ANY MORE. `--limit` and `--pagination-token` are gone and the API refuses them with a 400 naming --posts, because “fetch 20” and “fetch 15 then 5 more” must not be two prices for one answer. The 15-per-page walk still happens — internally, and `maxDepth` in the body is still how far into a history this read goes.
  • THIS IS THE UNTRACKED READ; `company-posts` IS THE TRACKED ONE. They are different commands against different endpoints: `company-posts` is a job against POST /api/v1/company/posts, it answers 404 “Profile not tracked” for anyone you have not tracked, and it PERSISTS every post it walks against that tracked source. This one is a synchronous GET, takes any public handle, and writes nothing anywhere.
  • IT COLLECTS NO ENGAGERS, AND THAT IS THE EXPENSIVE HALF YOU ARE SKIPPING. A tracked sweep finds posts through this same provider call and then fans out across the reactions and comments on each one — that fan-out is where a sweep’s cost and all of its LEADS come from. This returns the posts and their text and stops, and `captured: false` says so in the body: paying for a read does not make it a capture. If you wanted the people who engaged, you wanted `enrich-company --save-tracked-profile` (which tracks and sweeps and charges per LEAD).
  • A RATE LIMIT STILL BOUNDS IT, AND IT IS NOW THE SECONDARY GUARD RATHER THAN THE ONLY ONE. Both posts-read commands share ONE per-team budget of 10 CALLS A MINUTE, well below the 120/min ordinary reads get. A 429 HERE DOES NOT MEAN YOU ARE OUT OF CREDITS — that is a 402 with its own code — it means the two commands together were called too fast. Wait for the window the `Retry-After` header names instead of retrying in a loop.
  • AN EMPTY `posts` ARRAY IS A REAL ANSWER, NOT A FAILURE, AND IT COSTS NOTHING. It means that page has no posts we can read — a private page, one that only reshares, or one that has never posted. It is deliberately not a 404, because a 404 would be indistinguishable from a handle you typed wrong. A provider that would not answer at all is a 502 carrying its own code, and that charges nothing either.
  • `text` IS NULL, NEVER MISSING, when the provider served a post without any — normal for an image or video post. Every field of a post is present in every row for exactly this reason: read `null`, do not test for the key.
  • `--username` IS A COMPANY SLUG, AND ONLY A PERSON *URL* CAN BE REFUSED FOR YOU. Hand this command a linkedin.com/in/… URL and you get a 400 pointing at `profile-posts-read`, and it charges nothing. Hand it a bare person handle and you will NOT be warned: `jasonlemkin` and `instantlyapp` are the same shape of string, so it goes out as a company slug and comes back empty — which costs nothing, but is not evidence the person does not exist. When you hold a handle and do not know which kind it is, try `profile-posts-read` first.

get-icp

GET/api/v1/profile/{username}/icp

Show a tracked profile's ICP criteria (the filter rules + match mode behind isIcp / icpOnly).

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn profile username, not a full URL.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

set-icp

PUT/api/v1/profile/{username}/icp

Set a tracked profile's ICP criteria (rules and/or match mode). Re-scores existing leads' isIcp.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn profile username, not a full URL.
--match-modestringnoHow groups combine: 'all' (AND, default) or 'any' (OR).
--rulesjsonnoJSON array of { column, operator, value }. Pass [] to clear the ICP filter. e.g. '[{"column":"jobTitle","operator":"contains","value":"founder"}]'.
--dry-runbooleannoANSWER "what would this select?" WITHOUT APPLYING IT. The rules are validated and evaluated against this source's already-captured leads and NOTHING is written: no rules saved, no lead re-scored, no webhook fired. The reply carries `dryRun: true`, `counts` (matching / notMatching) and a `sample` of up to ten matching leads beside the `icp` it evaluated. RUN IT BEFORE ANY RULE CHANGE whose effect has not been seen: a real save re-scores every existing lead at once, which changes what an icpOnly webhook delivers and what a `--scope icp` push selects. The config evaluated is THIS call's flags over what is already stored, so `--dry-run --match-mode any` alone previews the stored rules under the new mode. Declaring neither --rules nor --match-mode is an error on a real save and a legitimate question here ("what does the config I have select?"). DELIBERATELY NOT DEFAULTED: this body is a PARTIAL UPDATE, so an omitted flag must put nothing on the wire.

Writing true, false and neither

True: --dry-run, --dry-run true, --dry-run=true. False: --no-dry-run, --dry-run false, --dry-run=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-dry-run.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

company-get-icp

GET/api/v1/company/{username}/icp

Show a tracked company page's ICP criteria (filter rules + match mode).

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company username, not a full URL.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

company-set-icp

PUT/api/v1/company/{username}/icp

Set a tracked company page's ICP criteria (rules and/or match mode). Re-scores existing leads' isIcp.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company username, not a full URL.
--match-modestringnoHow groups combine: 'all' (AND, default) or 'any' (OR).
--rulesjsonnoJSON array of { column, operator, value }. Pass [] to clear the ICP filter.
--dry-runbooleannoANSWER "what would this select?" WITHOUT APPLYING IT. The rules are validated and evaluated against this source's already-captured leads and NOTHING is written: no rules saved, no lead re-scored, no webhook fired. The reply carries `dryRun: true`, `counts` (matching / notMatching) and a `sample` of up to ten matching leads beside the `icp` it evaluated. RUN IT BEFORE ANY RULE CHANGE whose effect has not been seen: a real save re-scores every existing lead at once, which changes what an icpOnly webhook delivers and what a `--scope icp` push selects. The config evaluated is THIS call's flags over what is already stored, so `--dry-run --match-mode any` alone previews the stored rules under the new mode. Declaring neither --rules nor --match-mode is an error on a real save and a legitimate question here ("what does the config I have select?"). DELIBERATELY NOT DEFAULTED: this body is a PARTIAL UPDATE, so an omitted flag must put nothing on the wire.

Writing true, false and neither

True: --dry-run, --dry-run true, --dry-run=true. False: --no-dry-run, --dry-run false, --dry-run=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-dry-run.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

sync-status

GET/api/v1/profile/{username}/sync

Read a tracked profile's sync: `state`, `progress` counts, and `isFinal`. Synchronous read: it reports the CURRENT state and returns — nothing is polled for you. THE LIFECYCLE INCLUDES ENRICHMENT AND SO DOES `isFinal`, so `state` stays "enriching" and `isFinal` stays false while captured leads are still being enriched and charged; read `capture` when all you need is that collection finished, and `enrichment` for the pending/completed/failed/skipped breakdown. ⭐ WHY THE RUN ENDED WHERE IT DID IS `capture.stoppedBy`, AND WHAT IT COST IS `capture.creditsSpent`. `credits` means this source's own `--credit-cap-per-sync` was reached and THERE WAS MORE to collect — raise the limit to get it; `exhausted` means the run collected every engagement it found, so a bigger limit changes nothing. `capture_empty` means it collected POSTS and captured NOBODY from them — a CAPTURE failure, never a quiet week: for three days in September 2026 the upstream engager endpoints answered with no rows and every sync reported `completed` with no error. `errorCode` carries the same marker. Before these two, "collected 100 because that was everything" and "collected 100 because 100 was the cap" were the same output. `provider_limit` means the provider stopped serving (~1,100 people on a big post); `capture.coverage` gives declared vs captured. `creditsSpent` is what the run was CHARGED, `leadRowsCaptured` its lead rows; both on a FAILED run too. ⚠ BOTH ARE null UNTIL THE FIRST SYNC FINISHES, and `stoppedBy` is also null on a run that failed (neither word is true of a run that broke) and on one an untrack abandoned (the TOP-LEVEL `stoppedBy` says "untracked" for that). The TOP-LEVEL `stoppedBy` names the same ending, spelling the cap "credit_cap", plus "untracked".

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn profile username, not a full URL.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

company-sync-status

GET/api/v1/company/{username}/sync

Read a tracked company page's sync: `state`, `progress` counts, and `isFinal`. Synchronous read: it reports the CURRENT state and returns — nothing is polled for you. THE LIFECYCLE INCLUDES ENRICHMENT AND SO DOES `isFinal`, so `state` stays "enriching" and `isFinal` stays false while captured leads are still being enriched and charged; read `capture` when all you need is that collection finished, and `enrichment` for the pending/completed/failed/skipped breakdown. ⭐ WHY THE RUN ENDED WHERE IT DID IS `capture.stoppedBy`, AND WHAT IT COST IS `capture.creditsSpent`. `credits` means this source's own `--credit-cap-per-sync` was reached and THERE WAS MORE to collect — raise the limit to get it; `exhausted` means the run collected every engagement it found, so a bigger limit changes nothing. `capture_empty` means it collected POSTS and captured NOBODY from them — a CAPTURE failure, never a quiet week: for three days in September 2026 the upstream engager endpoints answered with no rows and every sync reported `completed` with no error. `errorCode` carries the same marker. Before these two, "collected 100 because that was everything" and "collected 100 because 100 was the cap" were the same output. `provider_limit` means the provider stopped serving (~1,100 people on a big post); `capture.coverage` gives declared vs captured. `creditsSpent` is what the run was CHARGED, `leadRowsCaptured` its lead rows; both on a FAILED run too. ⚠ BOTH ARE null UNTIL THE FIRST SYNC FINISHES, and `stoppedBy` is also null on a run that failed (neither word is true of a run that broke) and on one an untrack abandoned (the TOP-LEVEL `stoppedBy` says "untracked" for that). The TOP-LEVEL `stoppedBy` names the same ending, spelling the cap "credit_cap", plus "untracked".

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company username, not a full URL.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

track-post

POST/api/v1/post/track

Track a LinkedIn post and queue its first engagement capture. Reply authors are captured by default; `--no-capture-replies` opts out before lead writes or charges (CLI 4.4.0). Re-tracking an active post can change this setting without another capture; reactivating an untracked post queues a fresh capture.

FieldTypeRequiredDescription
--post-urlstringyesURL or URN of the post. Three forms: linkedin.com/feed/update/urn:li:activity:<id> (what you copy from your feed), linkedin.com/posts/<slug>-<id>-<hash> (the share-menu permalink), or a bare urn:li:activity:<id> / urn:li:ugcPost:<id>. All three are verified against LinkedIn first. A urn:li:share:<id> is rejected in every form — a share id is a different number from the activity id and cannot be mapped without the permalink.
--credit-cap-per-syncintegernoCapture cap counts lead rows; billing charges once per new person per source. The most enriching credits ONE SYNC of this tracked source may spend — THE CAP COUNTS LEAD ROWS. PER SYNC AND NOT A LIFETIME TOTAL: the source re-syncs on its own about every 24 hours and this bounds EACH of those runs, so 500 here authorises up to 500 credits every sync rather than 500 once. OMITTED LEAVES THE STORED LIMIT EXACTLY AS IT IS — on a source you already track as well as on a new one — so re-running this never silently removes a cap somebody set. Deliberately unbounded above: a source with no limit is uncapped, which is what every source created before this flag has been. A tracked post has no separate edit command — re-run `track-post` with a new value, which costs nothing because re-tracking a post you already have returns the existing source rather than re-syncing it. Read it back as `creditCapPerSync` on `sources`; a sync that ENDED on it reports `stoppedBy: "credit_cap"` on `source-sync-status`. NOT the keyword search field of the same old name: `track-keyword --credit-cap` is a search's per-RUN cap, a different number on a different source kind. PER SYNC, EVERY SYNC: it bounds each of those syncs, not the first pull only. NO ESTIMATE, NO CONFIRMATION GATE AND NO TEAM CEILING stand behind it — nothing prices a sync before you set the cap, raising or lowering it never needs --confirm-spend, and the team’s daily keyword ceiling does not count a credit of it. A keyword search’s --credit-cap HAS all three: it is the daily bound on a RECURRING SWEEP, `keyword-estimate` prices it, --confirm-spend gates a create or a raise, and the team’s `dailyCeiling` stops it — and that ceiling COUNTS KEYWORD SPEND ONLY, so no number of profile syncs can ever reach it.
--capture-repliesbooleannoCapture reply authors as leads (default true); false skips replies before lead writes and credits. Re-run track-post to change it for an existing post.
--enrich-leadsbooleannoEnrich this source's leads (default true). --no-enrich-leads (or --enrich-leads false) keeps them RAW: captured and charged exactly as before — one credit per NEW person per source, repeats free — but never enriched (no job title, company or country), and read only with `leads-raw`, never leads-list, engagers-list, exports, webhooks or integrations. Needs no --confirm-spend. Applies to leads captured after the change; omitted leaves the stored setting alone. CLI 4.7.0.

Writing true, false and neither

True: --capture-replies, --capture-replies true, --capture-replies=true. False: --no-capture-replies, --capture-replies false, --capture-replies=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-capture-replies, --no-enrich-leads.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

untrack-profile

DELETE/api/v1/profile/{username}

Stop tracking a LinkedIn profile — a SOFT DELETE, and storage and access are separate answers. Nothing is erased: the source is deactivated (status "inactive") and every lead it captured stays in the account. But those leads STOP BEING SERVED — leads-list and engagers-list drop them from the all-sources view and 404 on its id or username, and the source stops being actionable (no push, no webhook or ICP read/write). `sources --include-inactive true` enumerates what you used to track; that lists the source, it does not serve its leads. THE READ IS RE-OPENABLE ON EVERY SURFACE, THIS CLI INCLUDED SINCE 4.1.0: `leads-list --include-inactive true` and `engagers-list --include-inactive true` return what an untracked source kept and make its id resolve instead of 404ing, as GET /api/v1/leads?includeInactive=true and GET /api/v1/engagers?includeInactive=true do (MCP: list_leads / list_engagers with includeInactive). Reading is not acting: the push and the webhook/ICP routes answer 404 under that flag too, and the dashboard has no such switch at all. Re-tracking the same username revives that source and its leads are served again. IT ALSO STOPS WORK ALREADY RUNNING, which is the part that costs money: a sweep in flight is abandoned at the run's next CHECKPOINT — the run asks "am I still tracked?" before each provider call, before each post is harvested and before each chunk of lead rows — so expect it within moments, not at the instant this command returns (a provider call already in flight finishes first, and a failed status read is deliberately not a stop). Leads it had already written are KEPT and the run ends `stoppedBy: "untracked"` on its sync status. QUEUED ENRICHMENT IS WRITTEN OFF AND NOT CHARGED: enrichment lands minutes to hours after capture, so every lead of this source still waiting is marked `skipped` with the reason `source_untracked` and costs no enriching credits. Re-tracking the source returns exactly those leads to pending at the start of its next sync.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn profile username, not a full URL.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

track-keyword

POST/api/v1/keyword/track

Create a keyword search — find posts by up to 10 keywords and capture their engagers as leads, or with `--capture-post-authors` the person who wrote each post (CLI 4.6.0). Name it with `--name`, narrow what the sweep even looks at with the seven provider-side filters (`--author-industry`, `--author-company`, `--author-keyword`, `--from-person`, `--from-company`, `--mentions-person`, `--mentions-company`), filter with your own AI key via `--ai-provider`/`--ai-prompt` (the key itself is set per team per provider in the dashboard, never here), and bound the daily spend with `--credit-cap`. Reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).

FieldTypeRequiredDescription
--modestringnoengagers (default) or posts_only. Posts-only stores matching post text, captures no people/leads, and charges one credit per new kept post. Requires --posts-per-sync and --confirm-spend.
--posts-per-syncintegernoFor --mode posts-only, the daily maximum new posts to store and charge (1–60); repeat or rejected posts are free.
--capture-engagersbooleannoCapture the people who ENGAGED — likes and comments — on each kept post (default true, what every keyword search has always done). --no-capture-engagers (or --capture-engagers false) fetches no reactions or comments at all and needs --capture-post-authors, because a search must capture someone. Engagers mode only: refused with --mode posts-only. CLI 4.6.0.
--capture-post-authorsbooleannoCapture the person who WROTE each kept post, as a lead with engagementType Author (default false). Charged like an engager: ONE credit per NEW person for this search, repeats free (a later post by them, or them also liking it). A company-page author captures no one and costs nothing — counted as lastRun.companyAuthorsSkipped. With --no-capture-engagers a run adds at most one new person per kept post, still up to --credit-cap a day. Engagers mode only. CLI 4.6.0.
--namestringnoWhat to call this search in your source list, e.g. "hiring an SDR". Max 200 characters. Omit to use the keywords themselves.
--capture-repliesbooleannoCapture reply authors as leads (default true); false skips replies before lead writes and credits.
--enrich-leadsbooleannoEnrich this source's leads (default true). --no-enrich-leads (or --enrich-leads false) keeps them RAW: captured and charged exactly as before — one credit per NEW person per source, repeats free — but never enriched (no job title, company or country), and read only with `leads-raw`, never leads-list, engagers-list, exports, webhooks or integrations. Needs no --confirm-spend. Applies to leads captured after the change; omitted leaves the stored setting alone. CLI 4.7.0.
--keywordsjsonno1-10 search terms as a JSON array, e.g. '["AI agents","LLM evals"]'. Each is one provider call per run; results merge and dedupe, and the run's scan is shared round-robin. REQUIRED UNLESS YOU PASS --expression: exactly one of the two, and neither is a 400. A plain list is identical to the same terms joined with OR.
--date-postedstringyesPAST_24_HOURS | PAST_WEEK | PAST_MONTH. How far back each sweep looks. REQUIRED since 3.0.0 — one of the three scope decisions this command no longer guesses at. The dashboard pre-fills PAST_WEEK.
--sortstringnoRELEVANCE or DATE_POSTED.
--content-typestringnoVIDEO | IMAGE | JOB | LIVE_VIDEO | DOCUMENT | COLLABORATIVE_ARTICLE.
--author-industryjsonnoKeeps only posts whose author is in one of these industries. JSON array of NUMERIC LinkedIn industry ids, bare or wrapped, e.g. '["urn:li:industry:96"]'. An industry name is a 400 invalid_urn.
--author-companyjsonnoKeeps only posts whose author currently works at one of these companies. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn.
--author-keywordjsonnoJSON array of plain words matched against the AUTHOR (headline, title) — not the post. The one filter here that takes text rather than URNs, e.g. '["founder","head of sales"]'.
--from-personjsonnoKeeps only posts written by these people. JSON array of LinkedIn MEMBER IDS — "AC" plus base64url, about 39 characters — bare or wrapped, e.g. '["urn:li:person:ACoAAA5eqCEBzmkNfjyOp9_MseBpRQ-P17SuIos"]'. A HANDLE IS NOT ONE: a name or slug is a 400 invalid_urn. Resolve a handle for free with `cornersight profile-urn --username <handle>`.
--from-companyjsonnoKeeps only posts published by these company pages. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn.
--mentions-personjsonnoKeeps only posts that @mention one of these people. JSON array of LinkedIn MEMBER IDS — "AC" plus base64url, about 39 characters — bare or wrapped, e.g. '["urn:li:person:ACoAAA5eqCEBzmkNfjyOp9_MseBpRQ-P17SuIos"]'. A HANDLE IS NOT ONE: a name or slug is a 400 invalid_urn. Resolve a handle for free with `cornersight profile-urn --username <handle>`.
--mentions-companyjsonnoKeeps only posts that @mention one of these company pages. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn.
--ai-providerstringnoopenai | grok | gemini | claude — which STORED key filters the posts. The key itself is set in the dashboard and is never sent here.
--ai-modelstringnoModel id for the chosen provider. Optional even with --ai-provider: omit it and the provider default is used (openai gpt-6-luna, grok grok-4.3, gemini gemini-3.5-flash-lite, claude claude-haiku-4-5-20251001).
--ai-promptstringnoYour criterion. Required with --ai-provider.
--post-budgetintegernoRETIRED 30 Sep 2026 and IGNORED: "Posts per run" is no longer a setting. Accepted with any non-negative whole number, so older scripts keep running; the reply lists it in ignoredFields. --credit-cap is the one limit.
--credit-capintegeryesMaximum enriching credits one run may spend. REQUIRED since 3.0.0 — this is the number --confirm-spend authorises, and the one the daily charge is measured in; the dashboard pre-fills 100. PER RUN, and deliberately unbounded above — what stops a recurring search is your team's monthly balance, not this number — a sweep is skipped entirely once that is exhausted. It also bounds how many pages are FETCHED per post, so a high cap costs provider calls as well as storage.
--capture-modestringyesdepth | breadth — how the credit cap is spent across a run's posts. REQUIRED since 3.0.0; the dashboard pre-fills depth ("First posts"). depth lets each post spend the whole remaining cap, so the run goes deep on whatever it finds first (one viral post can take the lot). breadth shares the cap across the run's posts and redistributes the unspent, so the run spreads across more posts for fewer engagers each.
--max-engagements-per-postintegernoBreadth mode only: cap each post at this many engagements. Omit for no explicit ceiling (spread the whole credit-cap across posts). --credit-cap stays the hard limit; this only shapes the spend beneath it, so a small value across many posts may use fewer than --credit-cap credits — a deliberately narrower sweep. Ignored in depth mode.
--dry-runbooleannoPrint what this search would cost and create NOTHING. Runs `keyword-estimate` with these same flags: exit 0, the estimate as the usual JSON envelope on stdout, no search, no sweep, no charge. The three scope flags and --confirm-spend are NOT required with it — a dry run is the call you make in order to decide them. `resumed: true` in the reply means these keywords already name a search you have, and `previous` is what it is capped at now.
--confirm-changesbooleannoAcknowledge that this call would REWRITE settings on a search you already have. Only ever needed on a RESUME — when these keywords already name one of your searches and the flags you passed differ from what it is running on. Without it the API answers 409 `settings_conflict` and changes nothing, carrying `previous` (the whole current settings), the resulting settings and `changed` (each differing field with both values). ⚠ --confirm-spend DOES NOT acknowledge this — it authorises the recurring CHARGE and nothing else — and this flag never authorises a cap rise. Two changes, two flags: because this command requires --confirm-spend, it used to be the flag that silently covered a rewrite as well, which is why no track-keyword run had ever seen the refusal. A run that raises a cap AND rewrites a setting passes both; the API's spend refusal carries `changed` too, so one look tells you whether you need this.
--expressionstringnoA BOOLEAN SEARCH STRING INSTEAD OF --keywords, quoted for your shell: --expression 'hiring AND "sales ops" NOT recruiter OR fundraising'. ⚠ --expression AND --keywords ARE MUTUALLY EXCLUSIVE — sending both is a 400 and sending neither is a 400. NOT binds tighter than AND, which binds tighter than OR, and THERE ARE NO PARENTHESES, so `a AND b OR c` is `(a AND b) OR c`. Operators are UPPER CASE ONLY: a lower-case `and` is an ordinary search term, which is what keeps a saved term like `sales and marketing` meaning what it always meant. Only OR is served by the search itself, so every required term is still one provider call per run — AND and NOT are applied afterwards by reading each post's text, so a narrow expression costs the same and keeps fewer posts. --dry-run prices it exactly as it prices --keywords. A malformed expression is a 400 at save time naming the token at fault, and nothing is created.
--run-oncebooleannoHarvest ONCE, then stop scheduling. Without it the search recurs daily until you untrack it, which is what --confirm-spend authorises: a STANDING daily charge. A run that FAILED does not satisfy it, so this is one harvest, not one attempt. Omit for the daily cadence.
--end-atstringnoAn ISO 8601 UTC instant after which this search stops scheduling, e.g. 2026-12-01T00:00:00Z. MUST BE IN THE FUTURE — a date already past is a 400 and nothing is created. Checked before each run as well as after one, so a search whose end passed while it was idle never sweeps again. Omit for no end date.
--max-runsintegernoStop after this many COUNTED runs (1-3650). A run COUNTS when it reached the provider and ended ordinarily — the endings `sources --type keyword` reports as `lastRun.stoppedBy`: exhausted, credits, post_limit, or a team_cap/lead_cap that bound it mid-sweep. It does NOT count when the run FAILED (stoppedBy error or ai_error), when an untrack abandoned it, or when it was skipped before the provider was asked; a sweep that ran and captured nobody DOES count — `exhausted` means no capture cap ended the provider walk, and `lastRun.providerPageLimitReached` says whether its 20-page bound may have left later pages unread. So --max-runs 5 is at most five DAYS of the daily charge. Raise it later with keyword-update to restart a search that stopped at it. Omit for no run budget.
--confirm-spendbooleannoAuthorise the recurring daily charge this search creates. REQUIRED since 3.0.0 on every track-keyword — including one whose keywords RESUME a search you already have, which re-caps the live one. Without it this command prints what one day can cost and exits 1 WITHOUT sending anything. Pass --confirm-spend (or --confirm-spend true) once you have read that figure; --no-confirm-spend is the explicit refusal and exits 1 the same way.

Writing true, false and neither

True: --capture-engagers, --capture-engagers true, --capture-engagers=true. False: --no-capture-engagers, --capture-engagers false, --capture-engagers=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-capture-engagers, --no-capture-post-authors, --no-capture-replies, --no-enrich-leads, --no-confirm-changes, --no-run-once, --no-confirm-spend.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • THIS SEARCH RUNS EVERY DAY AND CHARGES EVERY DAY. It is not a one-off: once created it re-runs about every 24 hours until you untrack it, and --credit-cap applies PER RUN, not to the search's lifetime. The first sweep starts within seconds of creation, not a day later, and the daily cadence runs from there. CREATED, OR THE ONE YOU ALREADY HAVE: a keyword search's identity is its joined terms, so running this with keywords matching a search you already have — ACTIVE or one you DELETED — returns THAT search instead of making a second one: 200 with resumed:true rather than 201, and seenPosts says how many posts it has already swept and will therefore SKIP. A resumed search does NOT start clean. THE FLAGS YOU PASS ARE APPLIED TO THAT SEARCH AND THE ONES YOU OMIT ARE LEFT ALONE, so track-keyword with only --keywords returns the existing search unchanged rather than resetting its name, AI filter and caps to the defaults. Resuming a DELETED search also tracks it again and queues a sweep within seconds, which charges — run untrack-keyword if you did not mean to bring it back. Terms already held by another kind of source exit with identifier_in_use and create nothing. --credit-cap 2000 means up to 2000 credits EVERY DAY, not 2000 once. Set the caps to what one day may cost, and run untrack-keyword when you are done. AND THE REPLY SAYS WHAT ONE DAY COSTS, WHICH IS THE LINE TO READ OUT FIRST: `estimatedDailyMax` is the most this search can spend in a day in enriching credits — the same number the dashboard shows as "This search can cost up to N credits a day" above its Confirm button — and `daysToExhaustAtCap` is the whole days your remaining balance funds at that rate. Say the daily figure to whoever asked for the search BEFORE or AS you run this, never only afterwards: the first sweep starts within SECONDS and charges, so there is no gap in which to check it and no confirmation step on this command to stop it. `daysToExhaustAtCap: 0` is the WARNING and not "free" — your balance cannot fund one whole day at this cap, so the sweep this run just queued is the one that gets cut short. Both fields are OMITTED, never null and never 0, when there is no honest number (no usable --credit-cap, or a balance that could not be read), so test whether the field is PRESENT rather than reading a value out of it. `sources --type keyword` reports the same two on each search's `config`.
  • A RESUME APPLIES THE FLAGS YOU PASS TO THE SEARCH YOU ALREADY HAVE, AND THAT IS NOW REFUSED FIRST WHEN IT WOULD REWRITE WHAT THE SEARCH DOES. Running this with keywords that match a live search and a DIFFERENT --from-person, --sort, --date-posted, --content-type, any other targeting filter or any of the AI trio answers 409 `settings_conflict` and changes nothing. The body carries `previous` (the whole of the search's current settings), the resulting settings, and `changed` — each differing field with both values. Read `changed` to whoever asked, then re-run with --confirm-changes. It used to apply in SILENCE: --from-person A then --from-person B answered 200 resumed:true and left the one search holding B, with nothing in the reply to say so. ⚠ --confirm-spend DOES NOT ACKNOWLEDGE A SETTINGS CHANGE, and that correction is the point of this release. It did, and this command REQUIRES --confirm-spend — so every track-keyword run ever sent arrived pre-acknowledged, met this refusal not once, and went on rewriting live searches in exactly the silence described above. Two changes take two flags: --confirm-spend for the recurring charge, --confirm-changes for the rewrite, and a run that does both passes both. Use --dry-run first when you may be resuming: the estimate reports the same `previous` and `changed` and creates nothing. CLEARING the AI filter is the change to be most careful about: an unfiltered sweep keeps every post the terms return and charges a credit per engagement on all of them.
  • NOTHING IS SENT WITHOUT --confirm-spend, and the refusal happens HERE rather than at the API. Run track-keyword without it and the command prints what one day can cost and exits 1 having opened no connection — no search, no sweep, no charge. That is the only moment at which the answer can still be "then do not send it": a successful create starts its first sweep within seconds. THE THREE SCOPE FLAGS ARE REQUIRED FOR THE SAME REASON — --credit-cap, --capture-mode and --date-posted no longer have defaults, because a cap nobody chose still spends. --confirm-spend authorises the DAILY figure, not a total, and it is required on a resume too: matching keywords re-cap the search you already have. If you need the old behaviour, 2.11.0 is still on npm and unchanged — this is a major release for exactly that reason. TO SEE THE FIGURE WITHOUT SENDING ANYTHING, add --dry-run: the same command exits 0 with the estimate as JSON on stdout and creates nothing, which is what to run before you ask anyone to authorise it. The refusal above prints the number on stderr and exits 1; --dry-run is the same number as an answer rather than an error.
  • LOOK FIRST, WITH --include-inactive. `sources --type keyword --include-inactive true` is the only listing that shows STOPPED searches (status "inactive") alongside running ones, and a stopped search is precisely what these keywords may resume. Each row carries the terms it searches for as its `username` and the id `untrack-keyword` takes, so the check is one call before the one that charges. Without the flag the listing is ACTIVE-only, so a search you stopped last month is invisible right up to the moment this command brings it back and queues a sweep.
  • --credit-cap is the one limit you set, and the real bound on spend. A credit is charged per new person for this search, not per post or repeat engagement; 300 distinct new people can cost up to 300 credits and a 50-post sweep can cost many hundreds. It is PER RUN and has no maximum: what stops a recurring search is your team's monthly enriching-credit balance, because a sweep is skipped entirely once that is exhausted. The cap also decides how many pages of reactions and comments are FETCHED per post, so setting it high costs provider calls, not just rows.
  • A run stops at --credit-cap credits, when the provider runs out, or at 2,000 posts scanned (a posts_only search: --posts-per-sync posts). Each run sweeps only posts it has not captured before, so a post captured once is never re-swept and successive runs return new people rather than recharging for the same ones. WHEN AN EDIT TAKES EFFECT: FROM THE NEXT RUN, NEVER THE ONE ALREADY RUNNING. A sweep reads the search's settings ONCE, when it starts, and holds them for the whole run — so caps changed while a sweep is in flight do not re-bind it, and a run that began on the old caps finishes on them. An edit made BEFORE the sweep starts DOES bind that run, including one made after its job is queued, because the worker reads the settings when it picks the job up. `sources --type keyword` prints both halves: `config` (creditCap, captureMode, maxEngagementsPerPost) is what the NEXT run will use, and `lastRun.config` is the snapshot of what the LAST run was actually bound by — which is how you tell a real cap overrun from a run that started under the previous caps. `lastRun.config` is omitted, never back-filled, for runs that predate it.
  • WHY a run stopped is on `sources`, as the source's lastRun object: stoppedBy, at, postsScanned, postsKept, postsHarvested, providerRowsDropped, providerPageLimitReached. postsScanned->postsKept is the AI filter's before/after; postsHarvested is how far the run reached — posts it actually captured before stopping, which can be far below postsKept when a cap stops it early (kept 25, harvested 1). It counts a post swept to zero new leads (dedup) since the work was done. providerRowsDropped counts provider result rows skipped because they lacked a capturable activity URN, across terms and pages; 0 means a measured run skipped none, while an absent key means it was unmeasured. providerPageLimitReached is true when a term hit the 20-page safety bound before later pages were proven empty; false is a measured ordinary ending. engagersSeen, engagersDropped, engagersDuplicate and leadsWritten explain a run with few leads: dropped = seen but without any identity or an organization page, duplicate = already captured (free), seen 0 = the capture received nothing. All diagnostic fields are omitted rather than shown as 0 for runs predating them. credits = hit the credit cap (the real spend bound), post_limit = a posts_only search bought its postsPerSync posts, exhausted = the provider walk ended without a capture cap; read providerPageLimitReached before claiming no later pages remain. error = the sweep failed, ai_error = your own AI credential failed.
  • On a trial: at most 2 keyword searches per team, and each captures at most 250 leads.
  • --keywords takes a JSON ARRAY, quoted for your shell: --keywords '["AI agents","LLM evals"]'. A single term is still an array: '["AI agents"]'. Each term is one provider call per run, and the run's scan is shared round-robin across them.
  • --ai-provider and --ai-prompt must be given TOGETHER. Either one alone is a 400: the provider chooses which stored key to use, the prompt says what to keep, and neither is meaningful without the other.
  • The AI API key is NOT set here and cannot be sent to this command. Configure it once per team per provider in the dashboard; --ai-provider only selects which stored key to use.
  • Bounds that return 400: --keywords 1-10 terms, --name at most 200 characters, --credit-cap and --max-engagements-per-post whole numbers of 1 or more. A number outside its range is REJECTED, not rounded or replaced with the default — --credit-cap 0 used to be accepted and stored as 100. Since 3.0.0 --credit-cap, --capture-mode and --date-posted are REQUIRED and are refused locally, before any request: a missing one exits 1 with "Missing required flag". The API applies the same rule from ITS CUT-OVER, SCHEDULED FOR 2026-11-01, as 400 `scope_required` naming the field, and refuses an unconfirmed create with 409 `spend_confirmation_required` carrying the estimate — this command never reaches either, because it will not send an unconfirmed call. ⚠ THE CUT-OVER IS SCHEDULED FOR 2026-11-01, AND THIS IS THE NOTICE OF IT. UNTIL 2026-11-01 an old-style create — one that omits the three scope fields and `confirmSpend` — still returns 201/200 and carries a `deprecations` entry naming exactly what to add. FROM 2026-11-01 the same request is refused: a missing scope field is `400` `scope_required` naming the field, and a scope that is stated but not confirmed is `409` `spend_confirmation_required` — the same refusal a request pinned to `contractVersion: "2026-11-01"` already gets today, which is how you test the new behaviour before the date. THE EXACT CHANGE FOR A RAW HTTP CALLER IS FOUR KEYS: "creditCap": 100, "captureMode": "depth", "datePosted": "PAST_WEEK" and "confirmSpend": true. THIS COMMAND ALREADY REQUIRES THE EQUIVALENT — --credit-cap 100 --capture-mode depth --date-posted PAST_WEEK --confirm-spend — so a script that drives the CLI is compliant today and needs no change before the date. Those three values are what the dashboard's "Search scope" step pre-fills, offered so you can copy them DELIBERATELY — they are NOT server-side defaults and nothing is chosen for you, so send the caps you actually want, and send `confirmSpend` only once the person has heard the daily figure. THE VERSION STRING AND THE CUT-OVER DATE ARE THE SAME DAY, which they did not have to be: the version is what you PIN, the date is what happens to you if you do not.
  • The seven provider-side filters (--author-industry, --author-company, --from-person, --from-company, --mentions-person, --mentions-company) take LinkedIn URNs, NOT names — there is no name lookup. --author-keyword is the exception and takes plain words. All seven are JSON arrays, and all are optional: omit them for an unfiltered sweep.

untrack-keyword

DELETE/api/v1/keyword/{id}

Stop a keyword search — a SOFT DELETE: the source is deactivated (status "inactive") and sweeping, with the daily charge, stops. THE ONE KIND WHOSE LEADS STAY READABLE: what it captured is kept AND still served by leads-list and engagers-list, and the search stays pushable — the opposite of untracking a person, company page or post. Find its id again with `sources --include-inactive true`. IT ALSO STOPS WORK ALREADY RUNNING, which is the part that costs money: a sweep in flight is abandoned at the run's next CHECKPOINT — the run asks "am I still tracked?" before each provider call, before each post is harvested and before each chunk of lead rows — so expect it within moments, not at the instant this command returns (a provider call already in flight finishes first, and a failed status read is deliberately not a stop). Leads it had already written are KEPT and the run ends `stoppedBy: "untracked"` on its sync status. QUEUED ENRICHMENT IS WRITTEN OFF AND NOT CHARGED: enrichment lands minutes to hours after capture, so every lead of this source still waiting is marked `skipped` with the reason `source_untracked` and costs no enriching credits. Re-tracking the source returns exactly those leads to pending at the start of its next sync.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field track-keyword returned when it created the search, and the one `sources --type keyword` lists. Not a keyword string and not the search's name.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keyword-get-webhook

GET/api/v1/keyword/{id}/webhook

Show a keyword search's webhook configuration.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field track-keyword returned and the one `sources --type keyword` lists. A keyword search is addressed by id, not by its keyword text: its `username` in `sources` is the free-text terms it searches for.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keyword-set-webhook

PUT/api/v1/keyword/{id}/webhook

Configure a keyword search's webhook (partial update).

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field track-keyword returned and the one `sources --type keyword` lists. A keyword search is addressed by id, not by its keyword text: its `username` in `sources` is the free-text terms it searches for.
--webhook-urlstringnoPublic https URL to POST leads to. Pass an empty string to clear it.
--icp-onlybooleannoOnly deliver leads that match the source's ICP filter.
--auto-sendbooleannoAuto-deliver new leads (true) or deliver only on explicit push (false).
--sync-eventsbooleannoAlso send sync.completed/sync.failed callbacks (needs a webhook URL).

Writing true, false and neither

True: --icp-only, --icp-only true, --icp-only=true. False: --no-icp-only, --icp-only false, --icp-only=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-icp-only, --no-auto-send, --no-sync-events.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keyword-get-icp

GET/api/v1/keyword/{id}/icp

Show a keyword search's ICP criteria (filter rules + match mode).

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field track-keyword returned and the one `sources --type keyword` lists. A keyword search is addressed by id, not by its keyword text: its `username` in `sources` is the free-text terms it searches for.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keyword-set-icp

PUT/api/v1/keyword/{id}/icp

Set a keyword search's ICP criteria (rules and/or match mode). Re-scores existing leads' isIcp.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field track-keyword returned and the one `sources --type keyword` lists. A keyword search is addressed by id, not by its keyword text: its `username` in `sources` is the free-text terms it searches for.
--match-modestringnoHow groups combine: 'all' (AND, default) or 'any' (OR).
--rulesjsonnoJSON array of { column, operator, value }. Pass [] to clear the ICP filter.
--dry-runbooleannoANSWER "what would this select?" WITHOUT APPLYING IT. The rules are validated and evaluated against this source's already-captured leads and NOTHING is written: no rules saved, no lead re-scored, no webhook fired. The reply carries `dryRun: true`, `counts` (matching / notMatching) and a `sample` of up to ten matching leads beside the `icp` it evaluated. RUN IT BEFORE ANY RULE CHANGE whose effect has not been seen: a real save re-scores every existing lead at once, which changes what an icpOnly webhook delivers and what a `--scope icp` push selects. The config evaluated is THIS call's flags over what is already stored, so `--dry-run --match-mode any` alone previews the stored rules under the new mode. Declaring neither --rules nor --match-mode is an error on a real save and a legitimate question here ("what does the config I have select?"). DELIBERATELY NOT DEFAULTED: this body is a PARTIAL UPDATE, so an omitted flag must put nothing on the wire.

Writing true, false and neither

True: --dry-run, --dry-run true, --dry-run=true. False: --no-dry-run, --dry-run false, --dry-run=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-dry-run.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keyword-sync-status

GET/api/v1/keyword/{id}/sync

Read a keyword search's sweep: `state`, `progress` counts, and `isFinal`. Synchronous read: it reports the CURRENT state and returns — nothing is polled for you. THE LIFECYCLE INCLUDES ENRICHMENT AND SO DOES `isFinal`, so `state` stays "enriching" and `isFinal` stays false while captured leads are still being enriched and charged; read `capture` when all you need is that collection finished, and `enrichment` for the pending/completed/failed/skipped breakdown. ⚠ `capture.stoppedBy` IS ALWAYS null FOR A KEYWORD SEARCH: a sweep records its ending on the search, so read `lastRun.stoppedBy` on `sources` (budget, credits, exhausted, error, ai_error, team_cap, lead_cap). `capture.creditsSpent` is what the sweep charged, as `lastRun.creditsSpent`.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field track-keyword returned and the one `sources --type keyword` lists. A keyword search is addressed by id, not by its keyword text: its `username` in `sources` is the free-text terms it searches for.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keyword-update

PATCH/api/v1/keyword/{id}

Change a live keyword search's settings — its caps, its scope, its AI filter, reply capture, raw mode and provider-side targeting — without re-creating it. PARTIAL: only the flags you give change; everything else is left exactly as it is. Reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).

FieldTypeRequiredDescription
--modestringnoChange to engagers or posts_only. A new recurring spend mode needs --confirm-spend; the API returns a 409 quote without it.
--posts-per-syncintegernoBound a posts-only keyword source to 1–60 new paid posts per daily run. Raising it needs --confirm-spend.
--capture-engagersbooleannoCapture the people who liked or commented on each kept post. --no-capture-engagers stops fetching reactions and comments entirely and needs the search to capture post authors. Turning it back ON for a search that captured post authors only raises the daily maximum and needs --confirm-spend. Omitted leaves it as it is. Engagers mode only. CLI 4.6.0.
--capture-post-authorsbooleannoCapture the person who wrote each kept post as an Author lead — one credit per NEW person, repeats free, company-page authors skipped and not charged. Omitted leaves it as it is. Engagers mode only. CLI 4.6.0.
--idstringyesThe SOURCE id (a UUID) — the `id` field track-keyword returned and the one `sources --type keyword` lists. A keyword search is addressed by id, not by its keyword text: its `username` in `sources` is the free-text terms it searches for.
--capture-repliesbooleannoCapture reply authors as leads (default true); false skips replies before lead writes and credits.
--enrich-leadsbooleannoEnrich this source's leads (default true). --no-enrich-leads (or --enrich-leads false) keeps them RAW: captured and charged exactly as before — one credit per NEW person per source, repeats free — but never enriched (no job title, company or country), and read only with `leads-raw`, never leads-list, engagers-list, exports, webhooks or integrations. Needs no --confirm-spend. Applies to leads captured after the change; omitted leaves the stored setting alone. CLI 4.7.0.
--namestringnoWhat to call this search in your source list. Max 200 characters.
--date-postedstringnoPAST_24_HOURS | PAST_WEEK | PAST_MONTH. How far back each sweep looks.
--sortstringnoRELEVANCE or DATE_POSTED.
--content-typestringnoVIDEO | IMAGE | JOB | LIVE_VIDEO | DOCUMENT | COLLABORATIVE_ARTICLE.
--author-industryjsonnoKeeps only posts whose author is in one of these industries. JSON array of NUMERIC LinkedIn industry ids, bare or wrapped, e.g. '["urn:li:industry:96"]'. An industry name is a 400 invalid_urn. '[]' clears it.
--author-companyjsonnoKeeps only posts whose author currently works at one of these companies. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn. '[]' clears it.
--author-keywordjsonnoJSON array of plain words matched against the AUTHOR (headline, title) — the one filter here that takes text rather than URNs. '[]' clears it.
--from-personjsonnoKeeps only posts written by these people. JSON array of LinkedIn MEMBER IDS — "AC" plus base64url, about 39 characters — bare or wrapped, e.g. '["urn:li:person:ACoAAA5eqCEBzmkNfjyOp9_MseBpRQ-P17SuIos"]'. A HANDLE IS NOT ONE: a name or slug is a 400 invalid_urn. Resolve a handle for free with `cornersight profile-urn --username <handle>`. '[]' clears it.
--from-companyjsonnoKeeps only posts published by these company pages. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn. '[]' clears it.
--mentions-personjsonnoKeeps only posts that @mention one of these people. JSON array of LinkedIn MEMBER IDS — "AC" plus base64url, about 39 characters — bare or wrapped, e.g. '["urn:li:person:ACoAAA5eqCEBzmkNfjyOp9_MseBpRQ-P17SuIos"]'. A HANDLE IS NOT ONE: a name or slug is a 400 invalid_urn. Resolve a handle for free with `cornersight profile-urn --username <handle>`. '[]' clears it.
--mentions-companyjsonnoKeeps only posts that @mention one of these company pages. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn. '[]' clears it.
--ai-providerstringnoopenai | grok | gemini | claude — which STORED key filters the posts. The key itself is set in the dashboard and is never sent here. Moves with --ai-model and --ai-prompt.
--ai-modelstringnoModel id for the chosen provider. Optional even with --ai-provider: omit it and the provider default is used (openai gpt-6-luna, grok grok-4.3, gemini gemini-3.5-flash-lite, claude claude-haiku-4-5-20251001).
--ai-promptstringnoYour criterion. Required with --ai-provider.
--post-budgetintegernoRETIRED 30 Sep 2026 and IGNORED: "Posts per run" is no longer a setting. Accepted with any non-negative whole number, so older scripts keep running; the reply lists it in ignoredFields. --credit-cap is the one limit.
--credit-capintegernoMaximum enriching credits one run may spend, PER RUN — the search repeats about every 24 hours, so N is up to N credits EVERY DAY until you untrack it. RAISING it needs --confirm-spend; lowering it does not. Deliberately unbounded above: what stops a recurring search is your team's monthly balance.
--capture-modestringnodepth | breadth — how the credit cap is spent across a run's posts. Changing it does not change the cap, so it never needs --confirm-spend.
--max-engagements-per-postintegernoBreadth mode only: cap each post at this many engagements. --credit-cap stays the hard limit, so this only shapes the spend beneath it and can never raise it.
--run-oncebooleannoHarvest once, then stop scheduling. --no-run-once returns the search to the unbounded daily cadence — and on a search that already stopped FOR run-once, that RESTARTS it: the stop is cleared and the search is queued in this same call, so it sweeps and charges within about a minute.
--end-atstringnoAn ISO 8601 UTC instant after which this search stops scheduling. MUST BE IN THE FUTURE — a date already past is a 400 and nothing is written. Moving a passed end date forward RESTARTS a search that stopped for it, which queues a charging sweep within about a minute.
--max-runsintegernoStop after this many COUNTED runs (1-3650). RAISING IT ABOVE `schedule.runsCompleted` IS HOW A STOPPED SEARCH IS RESTARTED, and this command does both halves — it clears the stop AND re-queues the search, so the next sweep runs within about a minute rather than never. That resumes the daily charge: read `schedule` on `sources --type keyword` first. An edit that leaves the search stopped (a budget still at or below the runs it has had) deliberately does NOT clear the reason it is stopped.
--confirm-spendbooleannoAuthorise a RAISE of the daily figure (--credit-cap, or --posts-per-sync on posts_only). Needed only when this update increases it: a lowering, an equal value, and an update that touches neither are all accepted without it. Unlike track-keyword, this command sends without it and lets the API answer — a 409 here writes nothing, so the refusal costs nothing and it quotes the before, the after and the daily figure that a local check could not know.

Writing true, false and neither

True: --capture-engagers, --capture-engagers true, --capture-engagers=true. False: --no-capture-engagers, --capture-engagers false, --capture-engagers=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-capture-engagers, --no-capture-post-authors, --no-capture-replies, --no-enrich-leads, --no-run-once, --no-confirm-spend.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • RAISING WHAT ONE DAY CAN COST (--credit-cap, or --posts-per-sync on a posts_only search) NEEDS --confirm-spend. Without it the API answers 409 `spend_confirmation_required` and CHANGES NOTHING — no column written, no sweep re-bound — and the body carries `previous` (the caps the search holds now), the resulting caps, `raised` (which one went up) and `estimatedDailyMax`/`remainingBalance`/`daysToExhaustAtCap`. Show the person the before, the after and the daily figure, then re-run the identical command with --confirm-spend. Do NOT shrink the cap to get past the refusal. LOWERING a cap, re-sending the same cap, or changing anything else needs no confirmation — unlike `track-keyword`, this command does not refuse locally, because whether a cap is going UP depends on what the search already holds and only the server knows that.
  • PARTIAL, AND OMITTING A FLAG IS NOT THE SAME AS CLEARING IT: a flag you do not give leaves that column exactly as it is. The seven targeting filters clear with an empty array — `--author-industry '[]'` — because their columns are `text[] NOT NULL DEFAULT '{}'` and `[]` IS the unset state. ⚠ CLEARING A NULLABLE FIELD (--name, --content-type, --max-engagements-per-post and the --ai-provider/--ai-model/--ai-prompt trio) NEEDS AN EXPLICIT JSON `null`, WHICH NO STRING FLAG CAN EXPRESS — use PATCH /api/v1/keyword/{id} directly, or MCP `update_keyword`, for that one case. Everything else is reachable here.
  • THE AI TRIO MOVES AS ONE: naming any of --ai-provider, --ai-model or --ai-prompt rewrites all three, so re-state the provider when you change the prompt. A prompt with no provider is a 400 on every surface, and patching them independently is the only way to reach that state. --keywords IS NOT ACCEPTED AND IS A 400, NOT A SILENT DROP: a search's terms are its identity and its seen-set is keyed to the SEARCH rather than to a term, so new terms would inherit the old ones' swept posts. Create a search with the new terms and untrack this one. AN EDIT BINDS FROM THE NEXT RUN — a sweep reads its settings once, when it starts, so a run already in flight finishes on the caps it began with.

keyword-estimate

POST/api/v1/keyword/estimate

Price a keyword search WITHOUT creating it. Takes the same flags as track-keyword and returns what the next run would be bound by (creditCap, captureMode, datePosted), `estimatedDailyMax` — the most ONE DAY can cost in enriching credits — plus `remainingBalance` and `daysToExhaustAtCap`. Nothing is created, nothing is resumed and nothing is charged, so run it as often as you like. `cornersight track-keyword --dry-run` is the same request under the command you already know.

FieldTypeRequiredDescription
--modestringnoPrice engagers or posts_only before creating it. Posts-only quotes at most postsPerSync new post credits per day.
--posts-per-syncintegernoFor posts-only, the 1–60 new-post daily charge ceiling the estimate prices.
--capture-engagersbooleannoPrice with or without engager capture. --no-capture-engagers (with --capture-post-authors) prices a post-authors-only search (still up to creditCap a day). CLI 4.6.0.
--capture-post-authorsbooleannoPrice capturing each kept post's author as an Author lead — one credit per new person, like an engager. CLI 4.6.0.
--namestringnoWhat the search would be called. Does not affect the estimate; accepted so the flags you price are the flags you then create with.
--keywordsjsonno1-10 search terms as a JSON array, e.g. '["AI agents","LLM evals"]'. They decide whether `resumed` is true: a search's identity is its joined terms.
--expressionstringnoA BOOLEAN SEARCH STRING INSTEAD OF --keywords, quoted for your shell: --expression 'hiring AND "sales ops" NOT recruiter OR fundraising'. ⚠ --expression AND --keywords ARE MUTUALLY EXCLUSIVE — sending both is a 400 and sending neither is a 400. NOT binds tighter than AND, which binds tighter than OR, and THERE ARE NO PARENTHESES, so `a AND b OR c` is `(a AND b) OR c`. Operators are UPPER CASE ONLY: a lower-case `and` is an ordinary search term, which is what keeps a saved term like `sales and marketing` meaning what it always meant. Only OR is served by the search itself, so every required term is still one provider call per run — AND and NOT are applied afterwards by reading each post's text, so a narrow expression costs the same and keeps fewer posts. This command PRICES it without creating anything, which is the call to make first: a malformed expression is refused here too, in the create's own words. A malformed expression is a 400 at save time naming the token at fault, and nothing is created.
--date-postedstringnoPAST_24_HOURS | PAST_WEEK | PAST_MONTH. OPTIONAL here, unlike on track-keyword: omit it to price the current or default scope. Resolves to the live search's value, else PAST_WEEK.
--sortstringnoRELEVANCE or DATE_POSTED.
--content-typestringnoVIDEO | IMAGE | JOB | LIVE_VIDEO | DOCUMENT | COLLABORATIVE_ARTICLE.
--author-industryjsonnoKeeps only posts whose author is in one of these industries. JSON array of NUMERIC LinkedIn industry ids, bare or wrapped, e.g. '["urn:li:industry:96"]'. An industry name is a 400 invalid_urn. Counts towards `filtering.applied`.
--author-companyjsonnoAn organisation filter. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn. Counts towards `filtering.applied`.
--author-keywordjsonnoJSON array of plain words matched against the AUTHOR (headline, title) — the one filter that takes text rather than URNs. Counts towards `filtering.applied`.
--from-personjsonnoKeeps only posts written by these people. JSON array of LinkedIn MEMBER IDS — "AC" plus base64url, about 39 characters — bare or wrapped, e.g. '["urn:li:person:ACoAAA5eqCEBzmkNfjyOp9_MseBpRQ-P17SuIos"]'. A HANDLE IS NOT ONE: a name or slug is a 400 invalid_urn. Resolve a handle for free with `cornersight profile-urn --username <handle>`. Counts towards `filtering.applied`.
--from-companyjsonnoAn organisation filter. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn. Counts towards `filtering.applied`.
--mentions-personjsonnoKeeps only posts written by these people. JSON array of LinkedIn MEMBER IDS — "AC" plus base64url, about 39 characters — bare or wrapped, e.g. '["urn:li:person:ACoAAA5eqCEBzmkNfjyOp9_MseBpRQ-P17SuIos"]'. A HANDLE IS NOT ONE: a name or slug is a 400 invalid_urn. Resolve a handle for free with `cornersight profile-urn --username <handle>`. Counts towards `filtering.applied`.
--mentions-companyjsonnoAn organisation filter. JSON array of NUMERIC LinkedIn organisation ids, bare or wrapped, e.g. '["urn:li:organization:1441"]'. A company name is a 400 invalid_urn. Counts towards `filtering.applied`.
--ai-providerstringnoopenai | grok | gemini | claude — which STORED key would filter the posts. The key itself is set in the dashboard and is never sent here. With --ai-prompt it makes `filtering.applied` true.
--ai-modelstringnoModel id for the chosen provider. Omit it and the provider default is used (openai gpt-6-luna, grok grok-4.3, gemini gemini-3.5-flash-lite, claude claude-haiku-4-5-20251001). Does not affect the estimate.
--ai-promptstringnoYour criterion. Required with --ai-provider — half an AI filter is a 400 here exactly as it is on track-keyword.
--post-budgetintegernoRETIRED 30 Sep 2026 and IGNORED: "Posts per run" is no longer a setting. Accepted with any non-negative whole number, so older scripts keep running; the reply lists it in ignoredFields. --credit-cap is the one limit.
--credit-capintegernoMaximum enriching credits one run may spend, PER RUN. THIS IS THE NUMBER THE ESTIMATE IS ABOUT: `estimatedDailyMax` IS this value, and the sweep repeats about every 24 hours, so N is up to N credits EVERY DAY until you untrack it. Deliberately unbounded above — what stops a recurring search is your team's monthly balance. Omitted resolves to the live search's value, else 100.
--capture-modestringnodepth | breadth — how the credit cap would be spent across a run's posts. It reshapes the spend beneath the cap and never raises it, so it does not change `estimatedDailyMax`. Omitted resolves to the live search's value, else depth.
--max-engagements-per-postintegernoBreadth mode only: cap each post at this many engagements. Deliberately NOT part of `estimatedDailyMax` — a post whose engagement count the provider did not return is allocated the whole remaining cap, so a low per-post ceiling can still spend the full --credit-cap.

Writing true, false and neither

True: --capture-engagers, --capture-engagers true, --capture-engagers=true. False: --no-capture-engagers, --capture-engagers false, --capture-engagers=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-capture-engagers, --no-capture-post-authors.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • RUN THIS BEFORE track-keyword, AND READ THE NUMBER OUT. `estimatedDailyMax` is the most ONE DAY of the search can cost in enriching credits — the same figure the dashboard shows as "This search can cost up to N credits a day", and the same number track-keyword will report for the same flags, so quoting it is not an approximation. `daysToExhaustAtCap` is the whole days your balance funds at that rate, and `daysToExhaustAtCap: 0` is the WARNING and not "free": your balance cannot fund one whole day, so the very first sweep would be cut short. Both keys are OMITTED, never null and never 0, when there is no honest number (no usable --credit-cap, or a balance that could not be read), so test whether the key is PRESENT rather than reading a value out of it.
  • IT CREATES NOTHING. No search, no sweep, no charge, no change to a search you already have — this command only reads. That is what makes it safe to run twice with different caps to compare them, and it is why the three scope flags are OPTIONAL here while track-keyword requires them: this is the call you make in order to DECIDE them. Omit one and it is resolved the way the create would resolve it — the live search's value if these keywords name one, else the default (--credit-cap 100, --capture-mode depth, --date-posted PAST_WEEK).
  • `resumed: true` MEANS THESE KEYWORDS ALREADY NAME A SEARCH YOU HAVE — active, or one you stopped. track-keyword would then RESUME that search rather than create a second one: it applies the flags you pass, leaves the rest alone, and if the search had been stopped it tracks it again and queues a charging sweep within seconds. `previous` carries that search's CURRENT caps, so you can compare them with the ones you are asking for before anything happens. Use keyword-update, not track-keyword, when what you meant was to change a search you already have.
  • `filtering.applied` SAYS WHETHER THE SEARCH WOULD FILTER AT ALL, and when it is false the reply carries a `suggestion` written for a person plus `aiKeyStoredFor` — which of openai, grok, gemini and claude your team has a key stored for. An unfiltered sweep keeps every post the terms return and charges a credit per engagement on all of them, so this is the cheapest moment to add --author-keyword or the six URN filters. It is never a refusal: an unfiltered search is priced and created exactly as it always was.
  • `changed` SAYS WHAT track-keyword WOULD REWRITE ON THE SEARCH YOU ALREADY HAVE, and is the half `previous` alone could not: it lists every targeting filter, sort, datePosted, contentType or AI field whose value would differ, with both values. It is OMITTED entirely — never an empty array — when nothing would change, so test for the KEY. When it is present, the same call to track-keyword without --confirm-changes is a 409 `settings_conflict` — --confirm-spend authorises the charge only and does not acknowledge a rewrite, so a run that needs both passes both. `previous` is the search's WHOLE current settings, not only its four caps.
  • --keywords takes a JSON ARRAY, quoted for your shell: --keywords '["AI agents","LLM evals"]'. Same bounds as track-keyword, and the same 400s in the same order — a body this command prices is a body that command accepts.

untrack-post

DELETE/api/v1/post/{urn}

Stop tracking a post — a SOFT DELETE: the source is deactivated (status "inactive"), capture stops, and nothing it captured is erased. Its leads STOP BEING SERVED all the same — leads-list and engagers-list drop them from the all-sources view and 404 on its id, and the post stops being actionable. `sources --include-inactive true` still lists it, which says what you used to track rather than serving it again — `leads-list --include-inactive true` and `engagers-list --include-inactive true` read those kept leads back and resolve its id, as GET /api/v1/leads?includeInactive=true and GET /api/v1/engagers?includeInactive=true do (MCP: list_leads / list_engagers with includeInactive), while the dashboard has no such switch at all. Reading is not acting: the push and the webhook and ICP routes answer 404 under that flag too. IT ALSO STOPS WORK ALREADY RUNNING, which is the part that costs money: a sweep in flight is abandoned at the run's next CHECKPOINT — the run asks "am I still tracked?" before each provider call, before each post is harvested and before each chunk of lead rows — so expect it within moments, not at the instant this command returns (a provider call already in flight finishes first, and a failed status read is deliberately not a stop). Leads it had already written are KEPT and the run ends `stoppedBy: "untracked"` on its sync status. QUEUED ENRICHMENT IS WRITTEN OFF AND NOT CHARGED: enrichment lands minutes to hours after capture, so every lead of this source still waiting is marked `skipped` with the reason `source_untracked` and costs no enriching credits. Re-tracking the source returns exactly those leads to pending at the start of its next sync.

FieldTypeRequiredDescription
--urnstringyesURN of the tracked post, e.g. urn:li:activity:7123456789012345678.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

untrack-company

DELETE/api/v1/company/{username}

Stop tracking a LinkedIn company page — a SOFT DELETE, and storage and access are separate answers. Nothing is erased: the source is deactivated (status "inactive") and every lead it captured stays in the account. But those leads STOP BEING SERVED — leads-list and engagers-list drop them from the all-sources view and 404 on its id or username, and the source stops being actionable (no push, no webhook or ICP read/write). `sources --include-inactive true` enumerates what you used to track; that lists the source, it does not serve its leads. THE READ IS RE-OPENABLE ON EVERY SURFACE, THIS CLI INCLUDED SINCE 4.1.0: `leads-list --include-inactive true` and `engagers-list --include-inactive true` return what an untracked source kept and make its id resolve instead of 404ing, as GET /api/v1/leads?includeInactive=true and GET /api/v1/engagers?includeInactive=true do (MCP: list_leads / list_engagers with includeInactive). Reading is not acting: the push and the webhook/ICP routes answer 404 under that flag too, and the dashboard has no such switch at all. Re-tracking the same username revives that source and its leads are served again. IT ALSO STOPS WORK ALREADY RUNNING, which is the part that costs money: a sweep in flight is abandoned at the run's next CHECKPOINT — the run asks "am I still tracked?" before each provider call, before each post is harvested and before each chunk of lead rows — so expect it within moments, not at the instant this command returns (a provider call already in flight finishes first, and a failed status read is deliberately not a stop). Leads it had already written are KEPT and the run ends `stoppedBy: "untracked"` on its sync status. QUEUED ENRICHMENT IS WRITTEN OFF AND NOT CHARGED: enrichment lands minutes to hours after capture, so every lead of this source still waiting is marked `skipped` with the reason `source_untracked` and costs no enriching credits. Re-tracking the source returns exactly those leads to pending at the start of its next sync.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company username, not a full URL.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

profile-posts

POST/api/v1/profile/posts

Create a job that fetches posts for a tracked LinkedIn profile.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn profile username.
--limitintegernoPosts in this page, 1-15 (default 15, the most a page holds). A --pagination-token still follows while more posts exist. CLI 4.12.0.
--pagination-tokenstringnoPagination token returned by a previous posts response.
--no-waitbooleannoReturn immediately instead of polling the job to completion. The accepted job id is at data.jobId — read the result later with `job-status --job-id <data.jobId>`.default: false

What it prints

{ ok, jobId, data } — the JOB-STATUS response verbatim: data.status is the final state and data.result the payload. jobId sits beside data because the job-status response does not carry it. Read the payload at data.result. Polls GET /api/v1/jobs/{jobId}/status until the job is completed or failed, with no timeout. With --no-wait it prints { ok, data } instead and polls nothing — the accepted id is at data.jobId.

company-posts

POST/api/v1/company/posts

Create a job that fetches posts for a tracked LinkedIn company page.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company username.
--pagination-tokenstringnoPagination token returned by a previous posts response.
--no-waitbooleannoReturn immediately instead of polling the job to completion. The accepted job id is at data.jobId — read the result later with `job-status --job-id <data.jobId>`.default: false

What it prints

{ ok, jobId, data } — the JOB-STATUS response verbatim: data.status is the final state and data.result the payload. jobId sits beside data because the job-status response does not carry it. Read the payload at data.result. Polls GET /api/v1/jobs/{jobId}/status until the job is completed or failed, with no timeout. With --no-wait it prints { ok, data } instead and polls nothing — the accepted id is at data.jobId.

post-reactions

POST/api/v1/post/reactions

Create a job that fetches reactions for a tracked LinkedIn post.

FieldTypeRequiredDescription
--post-urnstringyesPost URN this team holds: tracked, or found by a keyword search or posts-only watch.
--pageintegernoZero-based page number.default: 0
--sourcestringnoOn a later page of the SAME sweep, the `source` (primary or fallback) the previous page's result reported, so the sweep stays on the provider serving this post. Omit on page 0; omitted later, the server uses whichever provider served page 0 of this post in the last hour. CLI 4.12.0.
--no-waitbooleannoReturn immediately instead of polling the job to completion. The accepted job id is at data.jobId — read the result later with `job-status --job-id <data.jobId>`.default: false

What it prints

{ ok, jobId, data } — the JOB-STATUS response verbatim: data.status is the final state and data.result the payload. jobId sits beside data because the job-status response does not carry it. Read the payload at data.result. Polls GET /api/v1/jobs/{jobId}/status until the job is completed or failed, with no timeout. With --no-wait it prints { ok, data } instead and polls nothing — the accepted id is at data.jobId.

post-comments

POST/api/v1/post/comments

Accepts ANY tracked post URN. --size selects 1-50 comments (default 50); --no-include-replies returns top-level comments only. HTTP 500 always falls back. ENGAGER_TRANSIENT_FALLBACK_ENABLED is off by default: 429/502/503/504 and transient network errors fall back only when on; otherwise they surface as errors.

FieldTypeRequiredDescription
--post-urnstringyesPost URN this team holds: tracked, or found by a keyword search or posts-only watch.
--pageintegernoZero-based page number.default: 0
--sizeintegernoComment rows (1-50; default 50).
--include-repliesbooleannoInclude replies (default true), or use --no-include-replies for top-level only.
--no-waitbooleannoReturn immediately instead of polling the job to completion. The accepted job id is at data.jobId — read the result later with `job-status --job-id <data.jobId>`.default: false

Writing true, false and neither

True: --include-replies, --include-replies true, --include-replies=true. False: --no-include-replies, --include-replies false, --include-replies=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-include-replies.

What it prints

{ ok, jobId, data } — the JOB-STATUS response verbatim: data.status is the final state and data.result the payload. jobId sits beside data because the job-status response does not carry it. Read the payload at data.result. Polls GET /api/v1/jobs/{jobId}/status until the job is completed or failed, with no timeout. With --no-wait it prints { ok, data } instead and polls nothing — the accepted id is at data.jobId.

company-post-comments

POST/api/v1/post/company-comments

Accepts ANY tracked post URN. --size selects 1-50 comments (default 50); --no-include-replies returns top-level comments only. HTTP 500 always falls back. ENGAGER_TRANSIENT_FALLBACK_ENABLED is off by default: 429/502/503/504 and transient network errors fall back only when on; otherwise they surface as errors.

FieldTypeRequiredDescription
--post-urnstringyesPost URN this team holds: tracked, or found by a keyword search or posts-only watch.
--pageintegernoZero-based page number.default: 0
--sizeintegernoComment rows (1-50; default 50).
--include-repliesbooleannoInclude replies (default true), or use --no-include-replies for top-level only.
--no-waitbooleannoReturn immediately instead of polling the job to completion. The accepted job id is at data.jobId — read the result later with `job-status --job-id <data.jobId>`.default: false

Writing true, false and neither

True: --include-replies, --include-replies true, --include-replies=true. False: --no-include-replies, --include-replies false, --include-replies=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-include-replies.

What it prints

{ ok, jobId, data } — the JOB-STATUS response verbatim: data.status is the final state and data.result the payload. jobId sits beside data because the job-status response does not carry it. Read the payload at data.result. Polls GET /api/v1/jobs/{jobId}/status until the job is completed or failed, with no timeout. With --no-wait it prints { ok, data } instead and polls nothing — the accepted id is at data.jobId.

job-status

GET/api/v1/jobs/{jobId}/status

Read the current status for a Public API job.

FieldTypeRequiredDescription
--job-idstringyesPublic API jobId returned by a job creation command.

What it prints

{ ok, data } — the job-status response verbatim. status, result and error are INSIDE data — data.status, data.result, data.error — and are NOT lifted to the top level; they were until 2.0.0. Read the payload at data.result. Synchronous: one request, no job, and --no-wait is not a flag it has.

credits

GET/api/v1/credits

Get the team's enriching credit balance (balance, used, limit, remaining, plan, nextReset).

Takes no flags of its own — --api-key is all it needs.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

credits-usage

GET/api/v1/credits/usage

Get a dated + by-source breakdown of enriching credits charged (defaults to the current billing period).

FieldTypeRequiredDescription
--fromstringnoISO 8601 start timestamp (default: current billing-period start).
--tostringnoISO 8601 end timestamp (default: now).

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keys-list

GET/api/v1/keys

List the team's active API keys (masked) and the per-team quota (used, max, remaining).

Takes no flags of its own — --api-key is all it needs.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keys-create

POST/api/v1/keys

Create a new named API key. The plaintext key is returned ONCE — store it now, it can't be retrieved again.

FieldTypeRequiredDescription
--namestringyesA label for the key (1-60 chars), e.g. "CI" or "Zapier".

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

keys-revoke

DELETE/api/v1/keys/{id}

Revoke (soft-delete) an API key by id — it stops authenticating immediately. Get the id from keys-list.

FieldTypeRequiredDescription
--idstringyesKey id from keys-list.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

leads-list

GET/api/v1/leads

List captured leads without re-sweeping posts. Each lead names its employer in companyName (also company), companyUrl, companyDomain, companyLinkedinUrl, companyDescription, companyIndustry, companyLocation, companyEmployeeCount, companyStaffRange and companyEnrichedAt; unknowns are null. Company fields come from the company record, which Cornersight resolves once per company and caches for every lead at that company. They cost no enriching credits. companyStaffRange is the LinkedIn size bucket and companyEmployeeCount is the reported total, so the two can disagree. companyEnrichedAt is null until the company has been resolved; after that, a null company field means the company record has no value for it. postPostedAt is the post's time; commentPostedAt is the comment's own time, null for Likes and when none could be obtained. --source-kind scopes totals to keyword, post or profile sources, including stopped searches with --include-inactive.

FieldTypeRequiredDescription
--limitintegernoPage size, 1-100 (default 50).
--offsetintegernoNumber of leads to skip (default 0).
--profile-idstringnoOnly leads for this tracked profile id. Mutually exclusive with --username.
--usernamestringnoOnly leads for this tracked LinkedIn username.
--engagement-typestringnoFilter by engagement kind: Like, Comment or Author (a keyword search's post author, captured with --capture-post-authors).
--is-icpstringnoFilter by ICP match: true or false.
--webhook-statusstringnoFilter by webhook state: pending, sent, failed, no_webhook.
--sincestringnoOnly leads stored at/after this ISO 8601 timestamp.
--untilstringnoOnly leads stored at/before this ISO 8601 timestamp.
--namestringnoSubstring match on the lead's name.
--job-titlestringnoSubstring match on job title.
--companystringnoSubstring match on company name.
--company-domainstringnoSubstring match on company domain.
--company-industrystringnoCase-insensitive substring match on the employer's industry. A lead whose industry is unknown does not match. CLI 4.12.0.
--countrystringnoSubstring match on country.
--include-syncingstringnotrue to also include profiles currently re-syncing (whose enriched leads are otherwise hidden until the sync finishes). Ignored with --profile-id/--username.
--include-inactivestringnoUse /api/v1/leads?includeInactive=true to read retained leads from stopped keyword, post, person and company sources, including by source id or username. Default false. Find stopped ids with `sources --include-inactive true`. Reading does not reactivate a source; push-leads, webhook and ICP actions still return 404.
--source-kindstringnoFilter all-source totals: keyword, post, profile (person and company), or all (default). Not combinable with --profile-id/--username.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

leads-raw

GET/api/v1/leads/raw

List RAW leads — people captured by sources in raw mode (`--no-enrich-leads`) and never enriched: { id, sourceId, linkedinUrl, linkedinUrn, name, action, commentText, post: { url, urn, postedAt }, detectedAt }, newest first. No job title, company or country; `name` is null when capture recorded only an id. Raw leads appear only here, never in leads-list, engagers-list, exports, webhooks or integrations (CLI 4.7.0).

FieldTypeRequiredDescription
--limitintegernoPage size, 1-100 (default 50).
--offsetintegernoNumber of raw leads to skip (default 0).
--profile-idstringnoOnly raw leads of this tracked source id. Mutually exclusive with --username.
--usernamestringnoOnly raw leads of this tracked LinkedIn username.
--actionstringnoOnly this engagement: Like, Comment or Author.
--sincestringnoOnly raw leads detected at/after this ISO 8601 timestamp.
--untilstringnoOnly raw leads detected at/before this ISO 8601 timestamp.
--include-inactivestringnotrue to also read raw leads of sources you have UNTRACKED, as leads-list does. Default false.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • RAW MODE COSTS WHAT ENRICHMENT COSTS: one credit per NEW person per source, charged at capture, repeats free. Reading here is free. Set it with `--no-enrich-leads` on enrich-profile/enrich-company (with --save-tracked-profile), track-post, track-keyword or the three *-update commands; it applies to leads captured after it. A raw source's sync status counts its raw leads as enrichment.completed and progress.leadsEnriched.
  • `total` counts engagements: a like and a comment by one person are two rows. Page with --limit/--offset and follow `hasMore`; for an incremental pull pass --since <the newest detectedAt you hold>.

engagers-list

GET/api/v1/engagers

List top engagers, one row per person with engagement count. Each engager names its employer in companyName (also company), companyUrl, companyDomain, companyLinkedinUrl, companyDescription, companyIndustry, companyLocation, companyEmployeeCount, companyStaffRange and companyEnrichedAt; unknowns are null. Company fields come from the company record, which Cornersight resolves once per company and caches for every lead at that company. They cost no enriching credits. companyStaffRange is the LinkedIn size bucket and companyEmployeeCount is the reported total, so the two can disagree. companyEnrichedAt is null until the company has been resolved; after that, a null company field means the company record has no value for it.

FieldTypeRequiredDescription
--limitintegernoPage size, 1-100 (default 50).
--offsetintegernoNumber of engagers to skip (default 0).
--min-engagementsintegernoOnly engagers with at least this many total engagements (default 1). Use 2+ for repeat engagers.
--order-bystringnoSort: engagementCount (default) or lastEngagedAt.
--profile-idstringnoOnly engagers of this tracked source id. Mutually exclusive with --username.
--usernamestringnoOnly engagers of this tracked LinkedIn username.
--engagement-typestringnoCount only this engagement kind: Like, Comment or Author (a keyword search's post author; counted in engagementCount, not likeCount or commentCount).
--is-icpstringnoOnly ICP-matching engagers: true or false.
--sincestringnoOnly engagements at/after this ISO 8601 timestamp.
--untilstringnoOnly engagements at/before this ISO 8601 timestamp.
--include-syncingstringnotrue to also include sources currently re-syncing. Ignored with --profile-id/--username.
--include-inactivestringnoUse /api/v1/engagers?includeInactive=true to read retained engagers from stopped keyword, post, person and company sources, including by source id or username. Default false. Find stopped ids with `sources --include-inactive true`. Reading does not reactivate a source; push-leads, webhook and ICP actions still return 404.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

source-posts

GET/api/v1/sources/{id}/posts

Read the posts a POSTS-ONLY watch has seen — the poll beside the `post.detected` push, the way `leads-list` is beside the `lead.detected` webhook. Reading charges NOTHING: the credit was spent when the post was first fetched and these are the same rows again.

FieldTypeRequiredDescription
--idstringyesThe tracked source id from `sources`: a posts-only person, company or keyword search. Engager sources answer 404.
--limitintegernoPosts to return, 1-200 (default 50). A value outside the range is a 400 naming it rather than a silent clamp.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • THIS IS FOR A POSTS-ONLY SOURCE (person, company or keyword), and answers 404 `not_posts_only` otherwise. An engagers source's posts are used for LEADS; an engagers keyword search's decisions are in `kept-posts`, which has no text.
  • IT COSTS NOTHING TO READ. A post is charged once, when the daily sync first fetches it; this re-serves the rows that charge already paid for. Read it as often as you like.
  • `firstSeenAt` IS WHEN WE SAW IT, NOT WHEN IT WAS POSTED, and both are returned. They differ by up to a day — the watch syncs about every 24 hours — and only the first explains why a three-day-old post arrived in this morning's webhook. The list is newest-first by `firstSeenAt`, which is the order the webhooks arrived in.
  • `--id` IS THE SOURCE ID from `sources`, not a handle. It is the same id `source-sync-status` and `source-get-webhook` take.
  • `text` IS NULL, NEVER MISSING, for a post the provider served without any — normal for an image or video post. Read `null`, do not test for the key.

kept-posts

GET/api/v1/sources/{id}/kept-posts

Which posts a keyword search's AI filter KEPT, with their permalinks. `sources` gives you lastRun's counts — scanned 25, kept 2 — and this tells you WHICH 2, which is what separates a filter that picked quiet posts from a capture that failed when engagersSeen is 0.

FieldTypeRequiredDescription
--idstringyesThe keyword source's id, as returned by `sources`. NOT a keyword string.
--includestringnoWhich set: `kept` (default) returns `keptPosts`; `swept` returns `sweptPosts`, every post the run considered with its outcome and its own engager and lead counts. Any other value is a 400. CLI 4.12.0.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • Read `scope` before comparing the list to lastRun.postsKept. `run` is the list THAT RUN recorded, in the order it walked the posts, and is the set postsKept counts. `prompt` is the fallback for a run that recorded none: every post kept under the CURRENT prompt across every run of it, which is a wider set and will not match postsKept. keptPosts is NULL with a `reason`, never an empty array, only on `prompt`, when nothing recorded a kept list: a run with NO AI filter keeps everything it scans and decides nothing, and a run from before verdicts were recorded kept posts without writing which. Both KEPT posts, so an empty array would falsely say they kept none. An empty array appears only on `run` and is measured: that run reached its filter and it rejected everything.
  • Each `urn` is the activity URN `post-reactions --post-urn` takes verbatim, which is the point: seeing postsKept 2 beside engagersSeen 0, run this and then post-reactions on each URN to check the posts yourself. A `url` of null means the post was kept but never harvested (a cap stopped the run first), so nothing was stored for it and post-reactions answers 404 for that URN.
  • A person, company or post id is a 404, not an empty result: those source kinds have no verdicts and never will.
  • WHEN THE COUNTS DO NOT ADD UP, use --include swept. It returns `sweptPosts`, one row per post the run considered, each with its `outcome` (scanned, kept, harvested or discarded) and its own engagersSeen, engagersDropped, engagersDuplicate and leadsWritten. Those rows SUM to lastRun's counters, so "harvested 25, 21 leads" becomes which posts wrote them and which wrote none. `sweptPosts` is null (with a `reason`), never [], when the run recorded no per-post list.

sources

GET/api/v1/sources

List the lead sources this team tracks: LinkedIn people, company pages, tracked posts, and keyword searches (each source's id is the profileId for leads-list). Filter with --type person|company|post|keyword. Posts are created with track-post, keyword searches with track-keyword (or the dashboard). A keyword source also carries `filters` — the seven targeting filters (--author-industry etc.) it was created with, present only when at least one is set, so you can read back what track-keyword was given. By default only ACTIVE sources are listed; --include-inactive true adds the untracked ones (status "inactive"), which is how a stopped keyword search's id is found again.

FieldTypeRequiredDescription
--typestringnoFilter by source type: person, company, post or keyword. An unrecognised value is a 400.
--include-inactivestringnotrue to also list UNTRACKED sources, each with status "inactive". The only way to find a stopped keyword search's id again — its leads are kept. An untracked profile, company or post is listed too; this lists SOURCES and does not serve leads — for the leads themselves, `leads-list --include-inactive true` and `engagers-list --include-inactive true` read back what such a source kept (GET /api/v1/leads?includeInactive=true and GET /api/v1/engagers?includeInactive=true over REST; MCP list_leads / list_engagers with includeInactive).

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • lastRun is present on KEYWORD sources only. On person, company and post sources it is null — those report their syncing through lastSyncedAt and nextSyncAt instead, so a null lastRun on a profile means the field does not apply, not that the profile has never run.
  • WHY a run stopped is on `sources`, as the source's lastRun object: stoppedBy, at, postsScanned, postsKept, postsHarvested, providerRowsDropped, providerPageLimitReached. postsScanned->postsKept is the AI filter's before/after; postsHarvested is how far the run reached — posts it actually captured before stopping, which can be far below postsKept when a cap stops it early (kept 25, harvested 1). It counts a post swept to zero new leads (dedup) since the work was done. providerRowsDropped counts provider result rows skipped because they lacked a capturable activity URN, across terms and pages; 0 means a measured run skipped none, while an absent key means it was unmeasured. providerPageLimitReached is true when a term hit the 20-page safety bound before later pages were proven empty; false is a measured ordinary ending. engagersSeen, engagersDropped, engagersDuplicate and leadsWritten explain a run with few leads: dropped = seen but without any identity or an organization page, duplicate = already captured (free), seen 0 = the capture received nothing. All diagnostic fields are omitted rather than shown as 0 for runs predating them. credits = hit the credit cap (the real spend bound), post_limit = a posts_only search bought its postsPerSync posts, exhausted = the provider walk ended without a capture cap; read providerPageLimitReached before claiming no later pages remain. error = the sweep failed, ai_error = your own AI credential failed.

discover

POST/api/v1/discover

Start a one-off Discover run: find people who post about a topic and get engagement on it. 1 credit per person added.

FieldTypeRequiredDescription
--keywordsjsonyesJSON array of 1-10 keywords; a post matching any counts.
--min-engagementintegeryesMinimum average likes + comments per post.
--max-influencersintegeryesMost people to add (1-500; trial 100), 1 credit each.
--countriesjsonnoOptional JSON array of up to 20 countries.
--confirm-spendbooleannoAuthorise up to --max-influencers credits.

Writing true, false and neither

True: --confirm-spend, --confirm-spend true, --confirm-spend=true. False: --no-confirm-spend, --confirm-spend false, --confirm-spend=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-confirm-spend.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • One-off, over the past month. Qualifies: AVERAGE likes + comments per matching post >= --min-engagement. Nothing is sent without --confirm-spend.

discover-list

GET/api/v1/discover

List Discover runs, newest first.

Takes no flags of its own — --api-key is all it needs.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

discover-get

GET/api/v1/discover/{id}

Show a Discover run and its influencers.

FieldTypeRequiredDescription
--idstringyesThe run id.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

discover-delete

DELETE/api/v1/discover/{id}

Delete a Discover run (soft; its leads are kept).

FieldTypeRequiredDescription
--idstringyesThe run id.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

push-leads

POST/api/v1/profile/{username}/push

Push already-captured leads for a tracked personal profile to its webhook — historical backfill, ICP-only or all, and the explicit retry for failed deliveries.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn profile username, not a full URL.
--scopestringnoWhich leads to select: 'all' (every eligible lead on the source) or 'icp' (only leads with isIcp true). Selection only — the webhook's own icpOnly flag still filters at delivery.default: "all"
--sincestringnoInclusive lower bound on the lead's detectedAt, ISO 8601 (e.g. 2026-08-01T00:00:00Z). Omit for 'from the beginning'.
--untilstringnoExclusive upper bound on detectedAt, ISO 8601. Omit and it is stamped with the moment the push is accepted, which is what makes the cohort historical and closed.
--idempotency-keystringnoRetry-safety opt-in. Re-running with the same key returns the first push's result (replayed: true) and queues nothing; the same key with different arguments is a 409.
--dry-runbooleannoCount the cohort and change nothing — no leads queued, no push recorded, the key not consumed.default: false

Writing true, false and neither

True: --dry-run, --dry-run true, --dry-run=true. False: --no-dry-run, --dry-run false, --dry-run=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-dry-run.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • HISTORICAL ONLY. A push selects leads by detectedAt over [since, until). An omitted --until is stamped with the moment the push is accepted, so leads captured after that belong to auto-send, not to this push. Saving a webhook does not deliver history; this command is how history is delivered. A push charges no credits.
  • ONLY ENRICHED LEADS ARE ELIGIBLE. The delivered payload is built from enrichment fields, so an un-enriched lead is never selected — it is not silently queued and lost.
  • --scope all AGAINST AN icpOnly WEBHOOK delivers only the ICP subset. The rest are queued, inspected and 'held' (reported by push-status), which is the usual reason an all-lead push delivers fewer than it selected.
  • AT MOST 2000 LEADS PER PUSH, oldest first. A larger cohort returns truncated: true and a nextSince — repeat with --since <nextSince> and a NEW --idempotency-key to continue.
  • DELIVERY IS AT-LEAST-ONCE and a re-push of an already-delivered lead is a legitimate replay, so the receiver must dedupe on data.leadId.

company-push-leads

POST/api/v1/company/{username}/push

Push already-captured leads for a tracked company page to its webhook — historical backfill, ICP-only or all, and the explicit retry for failed deliveries.

FieldTypeRequiredDescription
--usernamestringyesTracked LinkedIn company username, not a full URL.
--scopestringnoWhich leads to select: 'all' (every eligible lead on the source) or 'icp' (only leads with isIcp true). Selection only — the webhook's own icpOnly flag still filters at delivery.default: "all"
--sincestringnoInclusive lower bound on the lead's detectedAt, ISO 8601 (e.g. 2026-08-01T00:00:00Z). Omit for 'from the beginning'.
--untilstringnoExclusive upper bound on detectedAt, ISO 8601. Omit and it is stamped with the moment the push is accepted, which is what makes the cohort historical and closed.
--idempotency-keystringnoRetry-safety opt-in. Re-running with the same key returns the first push's result (replayed: true) and queues nothing; the same key with different arguments is a 409.
--dry-runbooleannoCount the cohort and change nothing — no leads queued, no push recorded, the key not consumed.default: false

Writing true, false and neither

True: --dry-run, --dry-run true, --dry-run=true. False: --no-dry-run, --dry-run false, --dry-run=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-dry-run.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • HISTORICAL ONLY. A push selects leads by detectedAt over [since, until). An omitted --until is stamped with the moment the push is accepted, so leads captured after that belong to auto-send, not to this push. Saving a webhook does not deliver history; this command is how history is delivered. A push charges no credits.
  • ONLY ENRICHED LEADS ARE ELIGIBLE. The delivered payload is built from enrichment fields, so an un-enriched lead is never selected — it is not silently queued and lost.
  • --scope all AGAINST AN icpOnly WEBHOOK delivers only the ICP subset. The rest are queued, inspected and 'held' (reported by push-status), which is the usual reason an all-lead push delivers fewer than it selected.
  • AT MOST 2000 LEADS PER PUSH, oldest first. A larger cohort returns truncated: true and a nextSince — repeat with --since <nextSince> and a NEW --idempotency-key to continue.
  • DELIVERY IS AT-LEAST-ONCE and a re-push of an already-delivered lead is a legitimate replay, so the receiver must dedupe on data.leadId.

keyword-push-leads

POST/api/v1/keyword/{id}/push

Push already-captured leads for a keyword search to its webhook, by source id — historical backfill, ICP-only or all, and the explicit retry for failed deliveries.

FieldTypeRequiredDescription
--idstringyesThe keyword search's source id (a UUID from `sources`). A keyword search is addressed by id, never by its keyword text.
--scopestringnoWhich leads to select: 'all' (every eligible lead on the source) or 'icp' (only leads with isIcp true). Selection only — the webhook's own icpOnly flag still filters at delivery.default: "all"
--sincestringnoInclusive lower bound on the lead's detectedAt, ISO 8601 (e.g. 2026-08-01T00:00:00Z). Omit for 'from the beginning'.
--untilstringnoExclusive upper bound on detectedAt, ISO 8601. Omit and it is stamped with the moment the push is accepted, which is what makes the cohort historical and closed.
--idempotency-keystringnoRetry-safety opt-in. Re-running with the same key returns the first push's result (replayed: true) and queues nothing; the same key with different arguments is a 409.
--dry-runbooleannoCount the cohort and change nothing — no leads queued, no push recorded, the key not consumed.default: false

Writing true, false and neither

True: --dry-run, --dry-run true, --dry-run=true. False: --no-dry-run, --dry-run false, --dry-run=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-dry-run.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • HISTORICAL ONLY. A push selects leads by detectedAt over [since, until). An omitted --until is stamped with the moment the push is accepted, so leads captured after that belong to auto-send, not to this push. Saving a webhook does not deliver history; this command is how history is delivered. A push charges no credits.
  • ONLY ENRICHED LEADS ARE ELIGIBLE. The delivered payload is built from enrichment fields, so an un-enriched lead is never selected — it is not silently queued and lost.
  • --scope all AGAINST AN icpOnly WEBHOOK delivers only the ICP subset. The rest are queued, inspected and 'held' (reported by push-status), which is the usual reason an all-lead push delivers fewer than it selected.
  • AT MOST 2000 LEADS PER PUSH, oldest first. A larger cohort returns truncated: true and a nextSince — repeat with --since <nextSince> and a NEW --idempotency-key to continue.
  • DELIVERY IS AT-LEAST-ONCE and a re-push of an already-delivered lead is a legitimate replay, so the receiver must dedupe on data.leadId.

source-push-leads

POST/api/v1/sources/{id}/push

Push already-captured leads for ANY tracked source to its webhook, by source id — the only push for a tracked post.

FieldTypeRequiredDescription
--idstringyesThe source id (a UUID from `sources`), for any kind.
--scopestringnoWhich leads to select: 'all' (every eligible lead on the source) or 'icp' (only leads with isIcp true). Selection only — the webhook's own icpOnly flag still filters at delivery.default: "all"
--sincestringnoInclusive lower bound on the lead's detectedAt, ISO 8601 (e.g. 2026-08-01T00:00:00Z). Omit for 'from the beginning'.
--untilstringnoExclusive upper bound on detectedAt, ISO 8601. Omit and it is stamped with the moment the push is accepted, which is what makes the cohort historical and closed.
--idempotency-keystringnoRetry-safety opt-in. Re-running with the same key returns the first push's result (replayed: true) and queues nothing; the same key with different arguments is a 409.
--dry-runbooleannoCount the cohort and change nothing — no leads queued, no push recorded, the key not consumed.default: false

Writing true, false and neither

True: --dry-run, --dry-run true, --dry-run=true. False: --no-dry-run, --dry-run false, --dry-run=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-dry-run.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • HISTORICAL ONLY. A push selects leads by detectedAt over [since, until). An omitted --until is stamped with the moment the push is accepted, so leads captured after that belong to auto-send, not to this push. Saving a webhook does not deliver history; this command is how history is delivered. A push charges no credits.
  • ONLY ENRICHED LEADS ARE ELIGIBLE. The delivered payload is built from enrichment fields, so an un-enriched lead is never selected — it is not silently queued and lost.
  • --scope all AGAINST AN icpOnly WEBHOOK delivers only the ICP subset. The rest are queued, inspected and 'held' (reported by push-status), which is the usual reason an all-lead push delivers fewer than it selected.
  • AT MOST 2000 LEADS PER PUSH, oldest first. A larger cohort returns truncated: true and a nextSince — repeat with --since <nextSince> and a NEW --idempotency-key to continue.
  • DELIVERY IS AT-LEAST-ONCE and a re-push of an already-delivered lead is a legitimate replay, so the receiver must dedupe on data.leadId.

push-status

GET/api/v1/push/{pushId}

Show the progress of a lead push: how many of its leads are pending, sending, delivered, failed or held, and whether it has finished.

FieldTypeRequiredDescription
--push-idstringyesThe pushId a push command returned.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • COUNTS ARE FOR THIS PUSH'S OWN COHORT, not for the source. A lead's webhookStatus is shared by every delivery path, so 'sent' on a lead does not say which push sent it — this does.
  • 'held' MEANS NOTHING WAS SENT: the delivery path had no destination for that lead, in practice an icpOnly webhook rejecting a non-ICP lead. It is a settled outcome, not a failure to retry.

source-sync-status

GET/api/v1/sources/{id}/sync

Check ANY tracked source's background capture sync by source id — stage, progress counts, and whether it has finished. The only way to follow a tracked post's lifecycle.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field `sources` prints for every kind of source. NOT a LinkedIn username, NOT a post URN, and NOT a keyword search's text: those are what `sources` shows as each source's `username`, which is a label, not an address here.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • ONE FAMILY, EVERY KIND OF SOURCE. These take the source `id` from `sources`, so they work for a person, a company page, a TRACKED POST and a KEYWORD SEARCH alike. `sync-status`/`company-sync-status` (by username) and `keyword-sync-status`/`keyword-get-webhook`/`keyword-set-webhook`/`keyword-get-icp`/`keyword-set-icp` (by id) still work and are unchanged; these are the ones that also reach a tracked post, which has no username to be named by.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person, company or post is neither readable nor actionable — its retained leads are out of `leads-list` too (re-openable over REST and MCP with ?includeInactive=true; leads-list and engagers-list have no such flag yet, and the push, webhook and ICP routes stay 404 under it in any case), and its webhook config is what a push would deliver to. A STOPPED KEYWORD SEARCH is the deliberate exception, because its leads are kept and served. `sources --include-inactive true` is what lists the untracked ones.
  • A MALFORMED --id IS A 400 saying so, never a 404: "that is not a source id" and "you do not have that source" are different answers, and the id of a source on another team gives the same 404 as one that does not exist.
  • THE LIFECYCLE INCLUDES ENRICHMENT, AND SO DOES `isFinal`. Capture writes the engagement records, then new people for this source are enriched for one credit each, while their repeat engagements are free, and that second half normally runs on well after capture ends. `state` reads `enriching` with `isFinal` false while leads are still being enriched and billed — read `enrichment` for the pending/completed/failed/skipped breakdown, and `capture` when all you need is that collection finished. It is the SAME object `sync-status` returns for a profile; only the addressing and the identity fields differ.
  • `track-post` RETURNS A syncId, and this command does NOT take it. Pass the SOURCE id (from `sources --type post`) as --id; the syncId is reported back to you here as `syncId`.

source-sync

POST/api/v1/sources/{id}/sync

Sync a person, company page or post now, by source id; charged like any sync.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field `sources` prints for every kind of source. NOT a LinkedIn username, NOT a post URN, and NOT a keyword search's text: those are what `sources` shows as each source's `username`, which is a label, not an address here.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • A sync in flight is returned (`queued: false`), not doubled. Keyword searches are refused.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person, company or post is neither readable nor actionable — its retained leads are out of `leads-list` too (re-openable over REST and MCP with ?includeInactive=true; leads-list and engagers-list have no such flag yet, and the push, webhook and ICP routes stay 404 under it in any case), and its webhook config is what a push would deliver to. A STOPPED KEYWORD SEARCH is the deliberate exception, because its leads are kept and served. `sources --include-inactive true` is what lists the untracked ones.
  • A MALFORMED --id IS A 400 saying so, never a 404: "that is not a source id" and "you do not have that source" are different answers, and the id of a source on another team gives the same 404 as one that does not exist.

source-get-webhook

GET/api/v1/sources/{id}/webhook

Show ANY tracked source's webhook configuration by source id — URL, ICP-only, auto-send and sync-events.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field `sources` prints for every kind of source. NOT a LinkedIn username, NOT a post URN, and NOT a keyword search's text: those are what `sources` shows as each source's `username`, which is a label, not an address here.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • ONE FAMILY, EVERY KIND OF SOURCE. These take the source `id` from `sources`, so they work for a person, a company page, a TRACKED POST and a KEYWORD SEARCH alike. `sync-status`/`company-sync-status` (by username) and `keyword-sync-status`/`keyword-get-webhook`/`keyword-set-webhook`/`keyword-get-icp`/`keyword-set-icp` (by id) still work and are unchanged; these are the ones that also reach a tracked post, which has no username to be named by.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person, company or post is neither readable nor actionable — its retained leads are out of `leads-list` too (re-openable over REST and MCP with ?includeInactive=true; leads-list and engagers-list have no such flag yet, and the push, webhook and ICP routes stay 404 under it in any case), and its webhook config is what a push would deliver to. A STOPPED KEYWORD SEARCH is the deliberate exception, because its leads are kept and served. `sources --include-inactive true` is what lists the untracked ones.
  • A MALFORMED --id IS A 400 saying so, never a 404: "that is not a source id" and "you do not have that source" are different answers, and the id of a source on another team gives the same 404 as one that does not exist.

source-set-webhook

PUT/api/v1/sources/{id}/webhook

Configure ANY tracked source's webhook by source id (partial update).

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field `sources` prints for every kind of source. NOT a LinkedIn username, NOT a post URN, and NOT a keyword search's text: those are what `sources` shows as each source's `username`, which is a label, not an address here.
--webhook-urlstringnoPublic https URL to POST leads to. Pass an empty string to clear it and stop delivery.
--icp-onlybooleannoOnly deliver leads that match this source's ICP filter. Pass false to deliver every lead.
--auto-sendbooleannoAuto-deliver new leads (true) or deliver only on an explicit push (false).
--sync-eventsbooleannoAlso send sync.completed/sync.failed callbacks (needs a webhook URL).

Writing true, false and neither

True: --icp-only, --icp-only true, --icp-only=true. False: --no-icp-only, --icp-only false, --icp-only=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-icp-only, --no-auto-send, --no-sync-events.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • ONE FAMILY, EVERY KIND OF SOURCE. These take the source `id` from `sources`, so they work for a person, a company page, a TRACKED POST and a KEYWORD SEARCH alike. `sync-status`/`company-sync-status` (by username) and `keyword-sync-status`/`keyword-get-webhook`/`keyword-set-webhook`/`keyword-get-icp`/`keyword-set-icp` (by id) still work and are unchanged; these are the ones that also reach a tracked post, which has no username to be named by.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person, company or post is neither readable nor actionable — its retained leads are out of `leads-list` too (re-openable over REST and MCP with ?includeInactive=true; leads-list and engagers-list have no such flag yet, and the push, webhook and ICP routes stay 404 under it in any case), and its webhook config is what a push would deliver to. A STOPPED KEYWORD SEARCH is the deliberate exception, because its leads are kept and served. `sources --include-inactive true` is what lists the untracked ones.
  • A MALFORMED --id IS A 400 saying so, never a 404: "that is not a source id" and "you do not have that source" are different answers, and the id of a source on another team gives the same 404 as one that does not exist.
  • PARTIAL UPDATE: only the flags you pass change, and a call with none of them is an error rather than a silent no-op. Each toggle takes an OPTIONAL value, so `--icp-only`, `--icp-only true` and `--icp-only=false` are all accepted, and `--no-icp-only` is the same as `--icp-only false` — an explicit false is a value, not an omission.
  • TO STOP DELIVERY, pass an empty --webhook-url. That also turns --sync-events off, because there is nowhere left to deliver; asking for --sync-events true with no URL is a 400 instead. The URL must be a public https host — localhost and private addresses are rejected on save by the same guard that blocks them at delivery, so a URL that saves is always one that can fire.
  • SAVING A URL DOES NOT DELIVER HISTORY. Already-captured leads are sent by `source-push-leads` (any kind, by id) or `push-leads` / `company-push-leads` / `keyword-push-leads`, which is also what `--auto-send false` leaves waiting for. A tracked post takes --webhook-url, --icp-only and --auto-send, and refuses --sync-events true.

source-get-icp

GET/api/v1/sources/{id}/icp

Show ANY tracked source's ICP criteria by source id (filter rules + match mode).

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field `sources` prints for every kind of source. NOT a LinkedIn username, NOT a post URN, and NOT a keyword search's text: those are what `sources` shows as each source's `username`, which is a label, not an address here.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • ONE FAMILY, EVERY KIND OF SOURCE. These take the source `id` from `sources`, so they work for a person, a company page, a TRACKED POST and a KEYWORD SEARCH alike. `sync-status`/`company-sync-status` (by username) and `keyword-sync-status`/`keyword-get-webhook`/`keyword-set-webhook`/`keyword-get-icp`/`keyword-set-icp` (by id) still work and are unchanged; these are the ones that also reach a tracked post, which has no username to be named by.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person, company or post is neither readable nor actionable — its retained leads are out of `leads-list` too (re-openable over REST and MCP with ?includeInactive=true; leads-list and engagers-list have no such flag yet, and the push, webhook and ICP routes stay 404 under it in any case), and its webhook config is what a push would deliver to. A STOPPED KEYWORD SEARCH is the deliberate exception, because its leads are kept and served. `sources --include-inactive true` is what lists the untracked ones.
  • A MALFORMED --id IS A 400 saying so, never a 404: "that is not a source id" and "you do not have that source" are different answers, and the id of a source on another team gives the same 404 as one that does not exist.

source-set-icp

PUT/api/v1/sources/{id}/icp

Set ANY tracked source's ICP criteria by source id (rules and/or match mode). Re-scores existing leads' isIcp.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field `sources` prints for every kind of source. NOT a LinkedIn username, NOT a post URN, and NOT a keyword search's text: those are what `sources` shows as each source's `username`, which is a label, not an address here.
--match-modestringnoHow columns combine: 'all' (AND, default) or 'any' (OR).
--rulesjsonnoJSON array of { column, operator, value }. Pass [] to clear the ICP filter.
--dry-runbooleannoANSWER "what would this select?" WITHOUT APPLYING IT. The rules are validated and evaluated against this source's already-captured leads and NOTHING is written: no rules saved, no lead re-scored, no webhook fired. The reply carries `dryRun: true`, `counts` (matching / notMatching) and a `sample` of up to ten matching leads beside the `icp` it evaluated. RUN IT BEFORE ANY RULE CHANGE whose effect has not been seen: a real save re-scores every existing lead at once, which changes what an icpOnly webhook delivers and what a `--scope icp` push selects. The config evaluated is THIS call's flags over what is already stored, so `--dry-run --match-mode any` alone previews the stored rules under the new mode. Declaring neither --rules nor --match-mode is an error on a real save and a legitimate question here ("what does the config I have select?"). DELIBERATELY NOT DEFAULTED: this body is a PARTIAL UPDATE, so an omitted flag must put nothing on the wire.

Writing true, false and neither

True: --dry-run, --dry-run true, --dry-run=true. False: --no-dry-run, --dry-run false, --dry-run=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-dry-run.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • ONE FAMILY, EVERY KIND OF SOURCE. These take the source `id` from `sources`, so they work for a person, a company page, a TRACKED POST and a KEYWORD SEARCH alike. `sync-status`/`company-sync-status` (by username) and `keyword-sync-status`/`keyword-get-webhook`/`keyword-set-webhook`/`keyword-get-icp`/`keyword-set-icp` (by id) still work and are unchanged; these are the ones that also reach a tracked post, which has no username to be named by.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person, company or post is neither readable nor actionable — its retained leads are out of `leads-list` too (re-openable over REST and MCP with ?includeInactive=true; leads-list and engagers-list have no such flag yet, and the push, webhook and ICP routes stay 404 under it in any case), and its webhook config is what a push would deliver to. A STOPPED KEYWORD SEARCH is the deliberate exception, because its leads are kept and served. `sources --include-inactive true` is what lists the untracked ones.
  • A MALFORMED --id IS A 400 saying so, never a 404: "that is not a source id" and "you do not have that source" are different answers, and the id of a source on another team gives the same 404 as one that does not exist.
  • PARTIAL UPDATE: pass --rules and/or --match-mode; a call with neither is an error. `--rules '[]'` CLEARS the filter, which makes every lead ICP again — that is a widening, not a narrowing.
  • EXISTING LEADS ARE RE-SCORED IMMEDIATELY, the same retroactive pass the dashboard runs, so `isIcp` filters and `icpOnly` delivery reflect the change now rather than at the next sync.

source-get-heyreach

GET/api/v1/sources/{id}/heyreach

Show any source's HeyReach auto-push: the campaign its new leads go to, by source id.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field `sources` prints for every kind of source. NOT a LinkedIn username, NOT a post URN, and NOT a keyword search's text: those are what `sources` shows as each source's `username`, which is a label, not an address here.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • `heyreach` is null when the source has none; `paused` says why auto-push stopped (invalid_key or campaign_closed).
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person, company or post is neither readable nor actionable — its retained leads are out of `leads-list` too (re-openable over REST and MCP with ?includeInactive=true; leads-list and engagers-list have no such flag yet, and the push, webhook and ICP routes stay 404 under it in any case), and its webhook config is what a push would deliver to. A STOPPED KEYWORD SEARCH is the deliberate exception, because its leads are kept and served. `sources --include-inactive true` is what lists the untracked ones.
  • A MALFORMED --id IS A 400 saying so, never a 404: "that is not a source id" and "you do not have that source" are different answers, and the id of a source on another team gives the same 404 as one that does not exist.

source-set-heyreach

PUT/api/v1/sources/{id}/heyreach

Send any source's new leads to a HeyReach campaign automatically, by source id.

FieldTypeRequiredDescription
--idstringyesThe SOURCE id (a UUID) — the `id` field `sources` prints for every kind of source. NOT a LinkedIn username, NOT a post URN, and NOT a keyword search's text: those are what `sources` shows as each source's `username`, which is a label, not an address here.
--campaign-idstringyesThe campaign's id, from `heyreach-campaigns`; "" removes the auto-push.
--auto-sendbooleannoPush new leads moving forward (default true).
--filtersjsonnoJSON array of { column, operator, value } rows; all must pass.
--push-historicbooleannoAlso send the leads already found that pass the filters, once.

Writing true, false and neither

True: --auto-send, --auto-send true, --auto-send=true. False: --no-auto-send, --auto-send false, --auto-send=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-auto-send, --no-push-historic.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • Enriched leads only; no LinkedIn profile URL is skipped; a person is never sent twice to the same campaign; a paused campaign is never resumed. --campaign-id "" removes it.
  • Filters are the leads tables' rows; a source has no ICP %, signal or engagement count (agent-only, refused). Choosing a campaign un-pauses it.
  • AN UNTRACKED SOURCE IS A 404, not an empty result: untracking is a soft delete, and a deactivated person, company or post is neither readable nor actionable — its retained leads are out of `leads-list` too (re-openable over REST and MCP with ?includeInactive=true; leads-list and engagers-list have no such flag yet, and the push, webhook and ICP routes stay 404 under it in any case), and its webhook config is what a push would deliver to. A STOPPED KEYWORD SEARCH is the deliberate exception, because its leads are kept and served. `sources --include-inactive true` is what lists the untracked ones.
  • A MALFORMED --id IS A 400 saying so, never a 404: "that is not a source id" and "you do not have that source" are different answers, and the id of a source on another team gives the same 404 as one that does not exist.

agent

GET/api/v1/agent

Show the Engagement Agent: profile, daily limit and split, status, webhook, people search and sources.

FieldTypeRequiredDescription
--agent-idstringnoWhich agent (see `agents`); omit for the first.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • `agent: null` until `agent-set`; `agents` lists them all.
  • RECURS DAILY until `agent-stop`, within the daily limit (`plan.total`); one credit per person checked. Trial: 1,000 people checked, up to 20 people.

agent-set

PUT/api/v1/agent

Create or update the Engagement Agent's profile, webhook and HeyReach auto-push. Partial; starts nothing, charges nothing.

FieldTypeRequiredDescription
--agent-idstringnoWhich agent (see `agents`); omit for the first.
--createbooleannoAdd another agent (another website) instead of changing one.
--websitestringnoThe team's website, e.g. acme.com; "" clears it.
--summarystringnoOne sentence on what the team sells.
--titlesstringnoJob titles sold to (max 15). Comma list or JSON array; "" clears.
--industriesstringnoLinkedIn industries (max 15). Comma list or JSON array; "" clears.
--company-sizesstringnoLinkedIn ranges: 1-10 ... 10001+. Comma list or JSON array; "" clears.
--countriesstringnoCountries (max 15). Comma list or JSON array; "" clears.
--topicsstringnoOne daily search per topic (max 20). Comma list or JSON array; "" clears.
--competitorsstringnoCompetitor names (max 8). Comma list or JSON array; "" clears.
--daily-creditsintegernoDaily credit limit, clamped to 50..20000; applies at the next agent-start. Ignored on a trial.
--webhookjsonnoJSON { url, autoSend, filters } or null; autoSend sends each new lead passing filters.
--heyreachjsonnoJSON { campaignId, autoSend, filters, pushHistoric } or null; HeyReach auto-push of Agent Leads.

Writing true, false and neither

True: --create, --create true, --create=true. False: --no-create, --create false, --create=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-create.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • Partial; a list passed replaces that list; over a cap is a 400. --create adds another agent (not on a trial).
  • RANKED, one row per person: title must match; ICP % 40-100 (title 40, country 30, industry 15, size 15; unknown earns half). Signal: Extra Strong (twice or more), Strong, Medium, Weak (hiring or personal post).

agent-start

POST/api/v1/agent/start

Start or restart the Engagement Agent: a daily search per topic and a one-off people search.

FieldTypeRequiredDescription
--agent-idstringnoWhich agent (see `agents`); omit for the first.
--daily-creditsintegernoDaily limit to start with (50..20000); omit for the profile's. Ignored on a trial.
--confirm-spendbooleannoAuthorise the recurring daily spend.

Writing true, false and neither

True: --confirm-spend, --confirm-spend true, --confirm-spend=true. False: --no-confirm-spend, --confirm-spend false, --confirm-spend=false. Omit it entirely and the field is not sent at all, so a partial update leaves the stored value unchanged. The --no- forms on this command: --no-confirm-spend.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • Without --confirm-spend: 409 `spend_confirmation_required` with `estimatedDailyMax`; nothing changes. Earlier agent sources stop first (leads kept).
  • RECURS DAILY until `agent-stop`, within the daily limit (`plan.total`); one credit per person checked. Trial: 1,000 people checked, up to 20 people.

agent-stop

POST/api/v1/agent/stop

Stop the Engagement Agent: untrack every source it set up; back to draft. Leads are kept.

FieldTypeRequiredDescription
--agent-idstringnoWhich agent (see `agents`); omit for the first.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • A running people search finishes but its people are not added.

agent-leads

GET/api/v1/agent/leads

List Agent Leads: one row per person, ranked by ICP % then signal, as the dashboard shows them.

FieldTypeRequiredDescription
--agent-idstringnoWhich agent (see `agents`); omit for the first.
--filtersstringnoThe table's Filters rows as JSON.
--limitintegernoPage size, 1-200 (default 50).
--offsetintegernoLeads to skip (default 0).

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • `checked` is everyone captured (paid for), `total` those shown; `firstRun.done` false while the first run runs.
  • RANKED, one row per person: title must match; ICP % 40-100 (title 40, country 30, industry 15, size 15; unknown earns half). Signal: Extra Strong (twice or more), Strong, Medium, Weak (hiring or personal post).

agent-push

POST/api/v1/agent/push

Send Agent Leads to the agent's webhook now: those passing its filters, or --lead-ids.

FieldTypeRequiredDescription
--agent-idstringnoWhich agent (see `agents`); omit for the first.
--lead-idsjsonnoJSON array of lead ids; omit for all passing the filters.

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • Push historic leads; never sent twice. 400 `no_webhook` without one.

heyreach

GET/api/v1/integrations/heyreach

Show the HeyReach connection: whether it is connected, whether HeyReach rejected the key, and every auto-push.

Takes no flags of its own — --api-key is all it needs.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • Connect the key on the dashboard's Integrations page; `keyRejected` true pauses auto-push until it is reconnected.

heyreach-campaigns

GET/api/v1/integrations/heyreach/campaigns

List the team's HeyReach campaigns that leads can join, with their LinkedIn senders.

Takes no flags of its own — --api-key is all it needs.

What it prints

{ ok, data } — the endpoint's response verbatim. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • 409 `not_connected` until HeyReach is connected; `canTakeLeads` is false for a campaign with no sender.

heyreach-send

POST/api/v1/integrations/heyreach/send

Add leads, or influencers found by Discover, to a HeyReach campaign.

FieldTypeRequiredDescription
--campaign-idstringyesThe campaign's id, from `heyreach-campaigns`.
--lead-idsjsonnoJSON array of lead ids (from `leads-list` or `agent-leads`).
--agent-idstringnoWith --lead-ids from `agent-leads`: adds ICP % and signal.
--peoplejsonnoJSON array of { profileUrl, name, jobTitle, company, country } (Discover influencers).

What it prints

{ ok, data } — the created resource verbatim — this endpoint answers with the thing it made, not with a jobId. Read the payload at data. Synchronous: one request, no job, and --no-wait is not a flag it has.

Before you run it

  • Enriched leads only; a lead with no LinkedIn profile URL is skipped; a person is never sent twice to the same campaign; a paused campaign is never resumed.
  • --lead-ids or --people, at most 1,000; --agent-id adds each Agent Lead's ICP % and signal as custom fields.