Cornersight CLI · v4.14.0

Cornersight from your terminal.

Enrich profiles, pull post engagement, and manage tracked sources and leads — scriptable, JSON-first, and built for pipelines.

cornersight — zsh
$ cornersight enrich-profile --username demo-profile --save-tracked-profile
{
  "ok": true,
  "jobId": "job_4f2c91",
  "data": {
    "status": "completed",
    "result": {
      "name": "Jordan Lee",
      "headline": "Founder & CEO at Acme",
      "company": "Acme Inc.",
      "location": "San Francisco, CA",
      "email": "jordan@acme.com"
    }
  }
}

Install

Current version v4.14.0, published as cornersight-cli (unscoped) — this is the version on npm, so it is what the commands below install. Check what you have with cornersight --version.

leads-list and engagers-list include company name, website URL and domain, LinkedIn company URL, description, industry, headquarters location, and employee size when available. In JSON, these are companyName (also company),companyUrl, companyDomain, companyLinkedinUrl,companyDescription, companyIndustry,companyLocation, companyEmployeeCount, companyStaffRange and companyEnrichedAt. Unknown values are null. companyUrl is the company's own website, from the website field of its company record, and never a LinkedIn URL; companyLinkedinUrl is its LinkedIn company page. 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. companyDescription and companyLocation (headquarters) come from the company record, resolved once per company and cached for 6 months, at no enriching-credit cost.

global install
npm install -g cornersight-cli

…or run without installing:

npx
npx cornersight-cli enrich-profile --api-key cs_your_key --username demo-profile --save-tracked-profile

Authentication

Every command takes --api-key cs_<your-key> — generate one in Cornersight → Settings → API key, or from the CLI itself with keys-create (keys-list shows the team's keys masked, keys-revoke kills one). A key is a full-team credential, so the plaintext is shown once and never again.

Two flags need neither a key nor a network call: --help, which prints the command list — or one command's flags — as JSON, and --version, which prints {"ok":true,"name":"cornersight-cli","version":"<version>"}. That is the version to quote in a bug report: it reads the installed manifest, so it answers what you are actually running rather than what is current.

Commands

All 73 of them, in the order the CLI itself lists them — related commands together. Each also accepts --api-key, and each maps to one endpoint in the API reference.

enrich-profileCreate a profile enrichment job. With `--save-tracked-profile`, reply authors are captured by default; `--no-capture-replies` opts out (CLI 4.4.0).
enrich-companyCreate 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-postsCreate a job that fetches posts for a tracked LinkedIn profile.
company-postsCreate a job that fetches posts for a tracked LinkedIn company page.
post-reactionsCreate a job that fetches reactions for a tracked LinkedIn post.
post-commentsAccepts 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-commentsAccepts 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.

The middle column is a usage line, not an inventory — it shows the required flags and the first few optional ones, then counts the rest. There are 321 flags across these commands, and track-keyword alone takes 32: the seven targeting filters that narrow a sweep before it runs (--author-industry, --author-company, --author-keyword, --from-person, --from-company, --mentions-person, --mentions-company), plus --ai-model, --capture-mode and --max-engagements-per-post. Every flag on every command, with its type and default, is in the flag reference, and cornersight <command> --help prints the same list for the version you have installed.

Six of those seven take an ID, never a name or a handle. --from-person and --mentions-person take a LinkedIn member id — "AC" plus base64url, about 39 characters — bare (ACoAAA5eqCEBzmkNfjyOp9_MseBpRQ-P17SuIos) or wrapped (urn:li:person:ACoAAA5eqCEBzmkNfjyOp9_MseBpRQ-P17SuIos); both are accepted and stored as the wrapped form. --author-company, --from-company and --mentions-company take a numeric organisation id (1441 or urn:li:organization:1441), and --author-industry a numeric industry id (96 or urn:li:industry:96). --author-keyword is the one that is plain words. Anything else is a 400 with code invalid_urn naming the field, the value, the expected shape and one example — on track-keyword, keyword-estimate and keyword-update alike. Have a handle and no id? cornersight profile-urn --username jasonlemkin resolves any public handle and charges nothing: it enriches nobody, tracks nobody, and every response carries creditsCharged: 0. Do not reach for enrich-profile instead — without --save-tracked-profile it answers 404 for anyone who is not already one of your leads, and with it, it tracks them, which queues a full sync and charges.

profile-posts-read and company-posts-read read what somebody has been posting without tracking them — up to --posts 1..60 from a public profile or company page, with their text, for a handle you do not monitor. One returned post costs one credit. Without --confirm-spend, the API returns a JSON 409 showing the estimated credits and remaining balance; the CLI prints it to stderr and exits 2 without fetching or charging. After confirming, the result includes creditsCharged and captured: false. The old --limit and --pagination-token flags are removed in 4.3.0. They are not profile-posts / company-posts, and the difference is not a detail: those are jobs against a tracked source, they answer 404 for anyone else, and they persist every post they walk. These two are one synchronous request each, take any public handle or company slug, and write nothing anywhere — no source row, no queued sync, no post row, no lead. What they do not do is collect engagers: the fan-out across each post's reactions and comments is the expensive half of a sweep and the half that produces every lead, so when the people who engaged are what you want, you want a tracked source and not these.

company-posts-read is the same read against a company page, and it differs in one place only: --username is a company slug — instantlyapp from linkedin.com/company/instantlyapp, with a full company URL accepted and unwrapped — and a linkedin.com/in/… person URL is refused with a 400 naming profile-posts-read rather than looked up as a company, so you are never left wondering whether a real person exists. The two share one rate budget, 10 calls a minute, tighter than the 120/min ordinary reads get. A 429 means called too fast (the CLI preserves Retry-Afterin its JSON error); a 402 means too few credits. Both walk provider pages internally to the maxDepth the response reports, and both need v4.3.0 or later, so a script that needs them should ask for cornersight-cli@>=4.3.0 rather than assuming whatever is already installed is new enough.

To watch new posts daily instead of making a one-off read, use enrich-profile or enrich-company with --save-tracked-profile --mode posts-only --posts-per-sync 1..60. Without --confirm-spend, the API refuses the recurring cost with a 409 and creates nothing. These flags also require CLI v4.3.0; the existing engagers mode remains the default.

--save-tracked-profile: create a tracked source (a profile/company you monitor), then enrich it. On a source that has already synced it applies the flags you pass and queues no sync — re-tracking does not re-sync, and the output says so with syncNotQueuedReason; source-sync --id <source-id> (CLI 4.9.0) syncs it now, charged like any sync. A lead is a different thing, a person captured from engaging with a tracked source's post, and a tracked source itself is not a lead. So enriching a source needs this flag every time (it's the mode selector). Without it, enrich-profile / enrich-company re-enrich an existing captured lead and return 404 No lead found for anyone who isn't yet a lead.

--credit-cap-per-sync: the optional spend limit on the source you are tracking, and the dashboard's Credit limit per sync. It allows at most N credits per sync, every sync — one credit is one lead collected — so it bounds each of the daily syncs rather than the first pull only, and it is never a lifetime total. Omit it and there is no limit: every sync collects every engagement it finds. It needs --save-tracked-profile, because a cap needs a tracked source to sit on, and profile-update / company-update change it later without re-syncing and without charging.

It is not the --credit-cap you will meet below. A per-sync cap has no estimate, no confirmation gate and no team ceiling: nothing prices a sync before you set it, 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, because it bounds a recurring daily sweep rather than one sync — keyword-estimate prices it, --confirm-spend gates a create or a raise, and the team's dailyCeiling stops it. That ceiling counts keyword spend only, so no number of profile syncs can ever reach it.

--first-sync-posts and --first-sync-days (CLI 4.10.0) choose what a new source's first sync collects: how many of the latest posts to take (1–50, default the latest 15), how many days back to go (1–90, at most 50 posts), or both (that many days back, at most --first-sync-posts posts). They shape the first sync only — every later sync checks the 4 newest posts, and a source that has already synced ignores them. Each collected post's engagers are charged as usual and --credit-cap-per-sync still bounds the sync. Like the cap they need --save-tracked-profile (refused locally without it), and the API refuses them with --mode posts-only, whose first run takes its --posts-per-sync newest posts.

Switching a source's mode. profile-update / company-update take --mode (CLI 4.12.0): --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), and --mode engagers turns it back. Only that second switch can raise the cost, so the API refuses it with 409 spend_confirmation_required and changes nothing until you re-run with --confirm-spend.

Also in 4.12.0: leads-list --company-industry, post-reactions --source (echo the previous page's source so a sweep stays on the provider serving that post), kept-posts --include swept (every post the run considered, with its own engager and lead counts) and profile-posts --limit (1–15 posts a page).

The Engagement Agent. CLI 4.13.0 adds agent, agent-set, agent-start, agent-stop, agent-leads and agent-push. Set the profile with, for example, agent-set --topics "cold email,AI SDR" --titles "VP Sales" --daily-credits 1000 (list flags take a comma list, or a JSON array for an entry that holds a comma; up to 20 topics). agent-start without --confirm-spend prints the API's 409 spend_confirmation_required with estimatedDailyMax and starts nothing; with it, the agent runs one search per topic every day and watches the people its one-off Discover search finds, until agent-stop. Half the daily limit goes to the topics; the other half watches people at up to 25 credits a day each. agent-leads lists Agent Leads as the dashboard does, one row per person: the job title must match, and the rest lowers the ICP % from 40 to 100 (title 40, country 30, industry 15, company size 15; an unknown part earns half). The signal is Extra Strong for engaging twice or more, Strong for a comment on a post about your topics or a lead-magnet post, at least Strong for a 100% ICP match on a post about your topics, and Weak only for a hiring post or personal news. --filters takes the table's filter rows, and checked is everyone the team pays for: one credit is one person checked. Until the first run finishes (never longer than a day) firstRun.done is false. agent-set --create adds another agent, one per website, and --agent-id picks one. --webhook takes { "url", "autoSend", "filters" }: autoSend is the dashboard's Push new leads moving forward, and agent-push is Push historic leads. On a free trial the agent has 1,000 people checked in all and watches up to 20 people.

HeyReach. CLI 4.14.0 adds heyreach, heyreach-campaigns, heyreach-send, source-get-heyreach and source-set-heyreach, and agent-set --heyreach. Connect HeyReach on the dashboard first; no command takes the key. heyreach-send --campaign-id 235 --lead-ids '["…"]' adds leads (or --people for Discover influencers) to a campaign. Only enriched leads are sent, and a lead with no LinkedIn profile URL is skipped. A person is never sent twice to the same campaign, and a paused campaign is never resumed. source-set-heyreach --id <source> --campaign-id 235 sends that source's new leads automatically (--push-historic sends the ones already found, once; --campaign-id "" removes it).

Raw leads: captured and charged, never enriched. --no-enrich-leads (CLI 4.7.0) puts a source in raw mode, on enrich-profile / enrich-company (with --save-tracked-profile), track-post and track-keyword, or later with profile-update, company-update or keyword-update; --enrich-leads switches it back. Its engagers are captured and charged exactly as before — one credit per new person per source, repeats free — but never enriched, so they carry no job title, company or country. Each raw lead has the person's LinkedIn URL and URN, their name when capture recorded one, the action (Like, Comment or Author), the comment text, the post's URL, URN and date, and when it was captured. Read them with leads-raw: they never appear in leads-list, engagers-list, the dashboard, exports, webhooks or integrations. The switch needs no --confirm-spend and applies to leads captured after it; leads already enriched stay enriched.

Keyword search is the fourth kind of tracked source, and the only one that finds its own posts. Instead of naming a person, a company or a single post, you give it search terms: every day it re-runs those terms, takes the posts they return, and captures the people who reacted and commented as leads — so it keeps finding new sources on its own. Create it with track-keyword, stop it with untrack-keyword, which deactivates the search and keeps everything already captured. Its leads land in the same place as every other source's, and sources --type keyword lists just these.

To watch matching posts without collecting engagers, use keyword-estimate --mode posts-only --posts-per-sync 5, then track-keyword --mode posts-only --posts-per-sync 5 --confirm-spend with the same terms and scope. This saves post text and charges one credit per new kept post, up to five on each daily run; previously seen and rejected posts are free. Read them with source-posts --id <source-id>.

--capture-post-authors (CLI 4.6.0) also captures the person who wrote each matching post, one credit per new person like an engager; company-page authors cost nothing. With --no-capture-engagers no reactions or comments are fetched. Filter them with leads-list --engagement-type Author.

Running it twice with the same terms is not an error and does not make a second search. A search is identified by its terms, so track-keyword with keywords you already have — active, or a search you deleted — returns that search, printing resumed: true and a seenPosts count of the posts it has already swept and will therefore skip. The flags you pass are applied to it and the ones you omit are left alone, so re-running with only --keywords gives it back unchanged rather than resetting its name, AI filter and caps to the defaults. Two things to know: resuming a deleted search tracks it again and queues a sweep within seconds, which charges — run untrack-keyword if you did not mean to bring it back; and terms already held by another kind of source (a profile, a company page, a tracked post) exit with identifier_in_use and create nothing. track-post behaves the same way for a post it already tracks, returning the existing source unchanged.

One limit bounds each daily run: --credit-cap caps the enriching credits one run may spend (the dashboard pre-fills 100). A run collects until it has spent it or the provider has no new posts. The optional AI filter is the part worth reading before you use it. --ai-provider and --ai-prompt must be given together — either one alone is a 400 — and the API key is not settable here. You store it once per team per provider in the dashboard, and --ai-provider only chooses which stored key to use; a key sent to this command is rejected, never used.

And the part that surprises people: it charges every day, not once. The cap above is per run, and a run happens roughly every 24 hours until you untrack-keyword it — so --credit-cap 2000 authorises up to 2,000 credits every day, indefinitely, not 2,000 in total. The first sweep starts within seconds of creation, not a day later, and the daily cadence runs from there. Pick caps you are happy to spend daily. --credit-cap is also the one that really bounds cost: a credit is charged per new person captured, not per post, so one post with 300 new reactors costs 300 credits. It has no maximum, and what actually stops a recurring search is your team's monthly enriching-credit balance — a sweep is skipped entirely once that is exhausted, so the series ends on its own rather than running forever. The cap also decides how many pages of reactions and comments are fetched per post, so setting it high costs provider calls, not just stored rows. Each run sweeps only posts it has not captured before, so later runs bring new people rather than recharging for the same ones. Why a run stopped — credits, post_limit, exhausted, error or ai_error — is on cornersight 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 actually captured before it stopped, which can be far below postsKept when a cap stops it early. providerRowsDropped counts provider result rows that lacked a capturable activity URN. budget and credits mean a capture cap ended the run; exhausted means the provider walk ended without one. Read providerPageLimitReached before concluding that no later provider pages remain. On a trial you may hold 2 searches, each capturing up to 250 leads.

Changing the caps changes the next run, not the one already running. A sweep reads the search's settings once, when it starts, and holds them for the whole run — so budgets edited 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 was queued, because the worker reads the settings when it picks the job up. cornersight 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. Compare a run's leadsWritten with that snapshot rather than with the current cap — 21 leads beside a stored cap of 5 is an overrun only if the snapshot says 5 as well. Runs older than the field omit it rather than reporting today's settings as theirs.

Since 3.0.0 nothing is sent without --confirm-spend. track-keyword creates a recurring daily charge, so three flags that used to have defaults are now required — --credit-cap, --capture-mode and --date-posted — because a cap nobody chose is still a cap that spends. Run the command without --confirm-spend and it 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 don't send it”, because a successful create starts its first sweep within seconds. --no-confirm-spend is the explicit no and exits the same way. The dashboard pre-fills a cap of 100, First posts and Past week; copy those deliberately rather than expecting a default, because there is none. This is a breaking change and cornersight-cli@2.11.0 stays on npm unchanged — pin it if you need the old behaviour while you decide the caps. The REST API is on a slower clock: it rolls the same contract out in a warning phase first, so an unchanged HTTP call still succeeds today and comes back carrying deprecations naming what to add and the cut-over date.

That cut-over is scheduled for 2026-11-01, and this is the notice of it. Until 2026-11-01 an old-style HTTP 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", "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 the dashboard's “Search scope” pre-fills, copied deliberately: they are not server-side defaults on either surface.

And the reply tells you what it will cost. track-keyword prints estimatedDailyMax — the most one day of this search can cost in enriching credits, the same number the dashboard shows above its Confirm button — and daysToExhaustAtCap, the whole days your remaining balance funds at that rate. Read the daily figure out to whoever asked for the search before or as you run the command: the first sweep starts within seconds and charges, so there is no gap in which to check it, and this command has no confirmation step. daysToExhaustAtCap: 0 is the warning rather than the all-clear — your balance cannot fund one whole day at that cap, so the sweep you 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. cornersight sources --type keyword carries the same two inside each search's config, so a search created months ago can be priced without re-creating it.

Examples

track & enrich a profile (waits for the job by default)
cornersight enrich-profile --api-key cs_your_key --username demo-profile --save-tracked-profile
# creates/syncs the tracked profile, enriches it, and prints the completed result as JSON
# (also starts capturing the profile's engagement as leads in the background)
re-enrich an existing captured lead (no flag)
cornersight enrich-profile --api-key cs_your_key --username some-lead-username
# only works for someone already captured as a lead — otherwise: 404 Lead not found
return immediately (only the seven job commands take --no-wait)
cornersight enrich-profile --api-key cs_your_key --username demo-profile --no-wait
# => {"ok":true,"data":{"jobId":"abc123"}}
# the id is at .data.jobId — the envelope is the same { ok, data } every command prints
cornersight job-status --api-key cs_your_key --job-id abc123
# => {"ok":true,"data":{"status":"completed","result":{...}}}
# status and result live INSIDE data; nothing is lifted to the top level
track a keyword search (re-runs daily)
cornersight track-keyword --api-key cs_your_key \
  --keywords '["outbound sales","cold email"]' --name "SDR buyers" \
  --credit-cap 300 --capture-mode depth --date-posted PAST_WEEK --confirm-spend
# one source that finds its own posts every day and captures their engagers as leads
# --keywords is a JSON array even for a single term: '["outbound sales"]'
…with the AI filter (both flags, and the key stays in the dashboard)
cornersight track-keyword --api-key cs_your_key \
  --keywords '["hiring an SDR"]' --name "hiring signal" \
  --ai-provider openai --ai-prompt "keep posts where the author is hiring salespeople"
# --ai-provider without --ai-prompt (or the reverse) is a 400
# the OpenAI key itself is set in the dashboard, per team per provider — never on this command
list keyword sources, then stop one
cornersight sources --api-key cs_your_key --type keyword
cornersight untrack-keyword --api-key cs_your_key --id <source-id>
# --id is the SOURCE id track-keyword returned (a UUID), not a keyword or the name
# untracking stops the daily run and the daily charge; leads already captured are kept
discover influencers: a one-off run (4.11.0+)
cornersight discover --api-key cs_your_key \
  --keywords '["claude code","ai agents"]' --min-engagement 50 --max-influencers 25 --countries '["UK"]'
# exits 1 and sends NOTHING: "This run can use up to 25 credits, once, not daily. ..."
# add --confirm-spend to start it: 1 credit per person added, at most --max-influencers
# a post matching ANY keyword counts; the run looks back one month, sorted by relevance;
# a person qualifies on their AVERAGE likes + comments per matching post; company pages are skipped
cornersight discover-list --api-key cs_your_key
cornersight discover-get --api-key cs_your_key --id <run-id>
# influencers, highest average first: name, linkedinUrl, jobTitle, company, country (null until enriched),
# highestEngagement, avgEngagement, postCount, topPostUrl, topPostText
cornersight discover-delete --api-key cs_your_key --id <run-id>
# soft delete: the run leaves the lists; its Author leads are kept. Full rules: /docs#discover
keep a source's leads raw, then read them
cornersight track-post --api-key cs_your_key --post-url "urn:li:activity:7123456789012345678" --no-enrich-leads
cornersight leads-raw --api-key cs_your_key --profile-id <source-id> --since 2026-09-01T00:00:00Z
# captured and charged like any lead (one credit per new person per source), never enriched,
# and returned only by leads-raw — leads-list, webhooks and exports never carry them
pipe into jq
cornersight post-reactions --api-key cs_your_key --post-urn "urn:li:activity:123" | jq '.data.result'
# every command prints { ok, data } — data is the API response verbatim, and is the only path
# (v1.x also lifted .result/.status to the top level; removed in 2.0.0)
cornersight credits --api-key cs_your_key | jq '.data'
# a synchronous command: one request, no job, no jobId, and --no-wait is not a flag it has

Output & exit codes

Success and errors are both JSON on stdout/stderr, so the CLI is safe to script.

In cornersight sources, a keyword search's lastRun.postsAvailableis the provider's approximate first-page total across terms (shared posts may count twice), and lastRun.caughtUp says every term returned only previously swept posts. Both are omitted when not measured.

0Success.
1Local/CLI error (bad flags, network).
2API error or the job failed.

Want an agent to drive the CLI? Point it at llms.txt.