Official JavaScript/TypeScript SDK and CLI for the SentiSense market intelligence API: stock prices, news and social sentiment, the SentiSense Score, the SentiSense Rating, insider and congressional trading, institutional 13F flows, options positioning, analyst ratings, earnings analysis, and a cross-signal screener.
- Full TypeScript support with detailed type definitions
- Works in Node.js 18+, Deno, Bun, and browsers
- Zero runtime dependencies (native
fetch) - Namespaced resources (
stocks,documents,institutional, ...) and a typed error hierarchy - A complete command line interface in the same package, runnable via
npxwith nothing to install
Get a free API key at app.sentisense.ai/get-api-key. Full API docs at sentisense.ai/docs/api.
npm install sentisenseimport SentiSense from "sentisense";
const client = new SentiSense({ apiKey: process.env.SENTISENSE_API_KEY });
const price = await client.stocks.getPrice("AAPL");
console.log(price.currentPrice);
// reportDate is optional; omit it to get the latest available quarter
// (this one returns a wrapper: see "Response shapes" below)
const flows = await client.institutional.getFlows();The same package ships a command line tool. Nothing to install:
npx -y sentisense@latest quote NVDAEvery endpoint needs a key, so set one first. Either works:
export SENTISENSE_API_KEY=<your key>
# or store it once, owner-readable only, at ~/.config/sentisense/config.json
npx -y sentisense@latest auth <your key>
npx -y sentisense@latest health| Command | What you get |
|---|---|
auth [key] |
Store a key, show what is configured, or --remove it |
health |
Reachability, key validity, latency, and the resolved base URL |
quote <ticker>... |
Price, day range, 52-week range, market cap, P/E. One request per ticker |
sentiment <ticker> |
SentiSense Score, tone, attention, per-source breakdown, --days N history |
mood |
Composite market sentiment, the signals behind it, and the sector map |
analysts <ticker> |
Consensus, price target band, recent upgrades and downgrades. --coverage for who covers it, by firm |
analyst <slug> |
One analyst: their firms, their coverage book, and --calls for their price target notes |
earnings [ticker] |
Forward calendar with no ticker, per-quarter analysis with one (earnings AAPL) |
insiders <ticker> |
Filed Form 4 transactions, including whether they were pre-planned |
insights <ticker> |
Generated signals, filterable by --urgency and --type |
congress [ticker] |
Congressional disclosures, market-wide or for one symbol |
news [ticker] |
Clustered news stories with impact and tone |
flows [ticker] |
Institutional 13F flows, or one ticker's holders and notable changes |
options <ticker> |
End-of-day options positioning, IV rank, walls, unusual contracts |
screen --filter ... |
Screen the universe on Score, analyst, technical, and price fields |
search <name> |
Resolve a name, alias, ticker or slug to a symbol and the entity handle |
Run sentisense help <command> for its flags and examples.
Three of these chain into each other. Start from a name, land on a person:
npx -y sentisense@latest search Tesla --type company # "Tesla" -> TSLA, plus the entity slug
npx -y sentisense@latest analysts NVDA --coverage # who covers it, by firm, with analyst slugs
npx -y sentisense@latest analyst quinn-bolton --calls # that analyst's firms, book, and notesanalyst takes a slug, not a name: slugs are lowercase and hyphenated, and every named analyst in a --coverage row carries the one that addresses them. A name is rejected before a request is spent. What comes back is call history, not accuracy scoring: there is no hit rate, no ranking, and nothing in it rates the person.
Ranking on search is the API's own, and a company can sort below its own products, so pass --type company when what you want is the issuer.
Readable in a terminal, plain text when piped, and exact API JSON on request:
npx -y sentisense@latest quote NVDA # terminal layout
npx -y sentisense@latest quote NVDA | cat # plain text, no escape codes
npx -y sentisense@latest quote NVDA --json | jq # the API response untouched--json prints what the API returned, envelope and all, so isPreview and totalCount stay visible. For quote that is the exact quote response for one ticker, and an object keyed by ticker for several. --full widens any command. --no-color and NO_COLOR drop the colour, --plain and --pretty force a layout, and --debug prints stack traces.
Commands spend requests on the answer, not on decoration: quote looks up the company name only for the terminal layout, so piped and --json output cost one request per ticker. When something supplementary does not come back, such as the Score history behind a sparkline, the command still prints its answer and exits 0 with a note: line on stderr, so stdout stays clean for a pipe and the gap is never silent.
Failures print two lines to stderr, what went wrong and what to do about it, and exit with a code you can branch on. The CLI does not retry, so a 5 is yours to handle.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | API error or unexpected failure |
| 2 | Bad usage: unknown command, flag, or missing argument |
| 3 | Missing or rejected API key |
| 4 | No data for that symbol or identifier |
| 5 | Rate limited |
| 6 | Network failure or timeout |
If you set SENTISENSE_AGENT_NAME (what your agent is called) and SENTISENSE_SKILL (the slug of the skill driving it), requests carry that identity, so usage can be understood and the tools improved. Both are optional, never required, and nothing is inferred when they are absent.
export SENTISENSE_AGENT_NAME=research-desk
export SENTISENSE_SKILL=us-stocks-analysis
npx -y sentisense@latest quote NVDA
# User-Agent: sentisense-node/{version} sentisense-cli/{version} (us-stocks-analysis; agent/research-desk)Either can also be a flag (--agent, --skill) or a stored setting (sentisense auth --agent research-desk --skill us-stocks-analysis), resolved flag first, then environment, then config. Values are reduced to letters, digits, dot, underscore and hyphen, and capped at 32 characters, so nothing you set can reshape the header.
Research data, not investment advice.
const client = new SentiSense({
apiKey: process.env.SENTISENSE_API_KEY, // Get yours at app.sentisense.ai/get-api-key
baseUrl: "https://...", // Default: https://app.sentisense.ai
timeout: 30000, // Default: 30s (in milliseconds)
maxRetries: 3, // Default: 3
userAgentSuffix: "my-bot/1.4", // Default: none
});| Option | Default | What it does |
|---|---|---|
apiKey |
none | Sent as X-SentiSense-API-Key. Required by every endpoint. |
baseUrl |
https://app.sentisense.ai |
Override for a non-production host. |
timeout |
30000 |
Per-request timeout in milliseconds. |
maxRetries |
3 |
Retries on 429 and 5xx, honouring Retry-After. Set 0 to fail fast. |
userAgentSuffix |
none | Appended to the User-Agent, after sentisense-node/{version}. |
userAgentSuffix is how you say what is calling on top of the SDK, so your traffic is legible in your own logs and in ours. A tool name and version works ("my-bot/1.4"), optionally with an agent label ("my-bot/1.4 agent/research-desk"). Node only, since browsers set the header themselves. Newlines are collapsed and an empty value is ignored.
Keep the key in the environment rather than in source. Committing a literal key leaks it into git history and into every registry security scan that reads your repo.
Most methods resolve to the payload directly, but two families wrap it. The return types describe the wrapper, so .data / .documents type-check natively, no cast.
1. Tier-gated endpoints return a preview envelope. The payload is in data, and isPreview tells you whether it was truncated for your tier. totalCount carries the untruncated size whenever the server knows it: on a truncated response, so you can render "showing N of M", and on a paged endpoint such as politicians.getActivity, where it is the full match count on every tier including PRO. A missing totalCount means "count data yourself", never "zero results".
Affected: every method whose declared return type is PreviewResponse<T>. A test keeps this table in step with the source, so it is the full list rather than a sample.
| Namespace | Methods |
|---|---|
analyst |
consensus actions estimates marketActivity coverage profile calls |
calendar |
getEarnings |
earnings |
getSummaries getRecent |
etfs |
analystAggregate insiderAggregate sentimentAggregate |
insider |
getActivity getTrades getClusterBuys |
insights |
stock stockRange market latest user |
institutional |
getFlows getHolders getActivists |
options |
getOverview |
politicians |
getActivity getFilings getMembers getMember |
stocks |
getSentiment getKpis getOptionsSummary getOptionsHistory |
The envelope itself is always an object, so test the payload rather than the response. Two of these declare a data that can be null: stocks.getOptionsSummary, for a ticker outside the covered options universe, and options.getOverview, before its first nightly build. Everywhere else data is an array or an object.
const flows = await client.institutional.getFlows();
if (flows.isPreview) {
console.log(`Preview: ${flows.data.inflows.length} of ${flows.totalCount}`);
}
for (const flow of flows.data.inflows) {
console.log(flow.ticker, flow.netSharesChange);
}
// holders nest one level deeper: ticker-level totals plus the rows
const holders = await client.institutional.getHolders("AAPL", "2026-06-30");
console.log(`${holders.data.holderCount} holders`);
const newPositions = holders.data.holders.filter((h) => h.changeType === "NEW");
// insights use the same envelope, wrapping a plain array
const insights = await client.insights.stock("AAPL");
for (const insight of insights.data) {
console.log(insight.insightText);
}2. Document endpoints return a search wrapper. This is not the preview envelope: the rows are in documents and there is no isPreview.
Affected: documents.getByTicker / getByTickerRange / getByEntity / search / getBySource. Also stocks.getFundamentalsPeriods, whose periods are in periods.
const results = await client.documents.search("NVDA earnings", { days: 7 });
console.log(`${results.totalCount} matches`);
for (const doc of results.documents) {
console.log(doc.url, doc.averageSentiment);
}Everything else, including stocks.getPrice(), documents.getStories(), insights.types() and institutional.getQuarters(), resolves to the value itself with no wrapper.
Upgrading from 0.28.x or earlier? These return types were corrected in 0.29.0. If your code read the flat shape (
flows.inflows,holders.filter(...)), it was returningundefined/ throwing at runtime already; switch toflows.data.inflows/holders.data.holders. See CHANGELOG.md for the full mapping.
client.stocks.list() // All ticker symbols
client.stocks.listDetailed() // All stocks with details
client.stocks.getPrice("AAPL") // Latest price
client.stocks.getPrices(["AAPL", "NVDA"]) // Batch prices
client.stocks.getQuote("AAPL") // Fuller quote: ranges, market cap, P/E
client.stocks.getProfile("AAPL") // Company profile
client.stocks.getChart("AAPL", { timeframe: "6M" }) // OHLCV chart data
client.stocks.getMarketStatus() // Market open/closed
client.stocks.getFundamentals("AAPL") // Financial data
client.stocks.getShortInterest("GME") // Short interest
client.stocks.getOptionsSummary("NVDA") // End-of-day options dossier
client.stocks.getOptionsHistory("NVDA", { window: "2y" }) // Daily options aggregates over time
client.stocks.getRating("AAPL") // SentiSense Rating: score, letter, percentile, dimensions
client.stocks.getAISummary("AAPL", { depth: "deep" }) // AI report (PRO)Price fields carry priceAsOf (Unix milliseconds) for the age of the market data; read that for freshness rather than timestamp, which is when the response was served.
client.documents.getByTicker("AAPL", { source: "news", days: 3 })
client.documents.search("NVDA earnings", { days: 7, limit: 20 })
client.documents.getStories({ limit: 10 })
client.documents.getStoryDetail("cluster_abc123")A story's cluster says where it came from and whether it has settled. storySource is
"ORIGINAL" for an editorially authored SentiSense Original and "AI" for a
pipeline-generated story, and isLive is true while the story is still being revised as
the event develops. Both are optional: against an API build that predates them they are
undefined, which means "not known" rather than "AI" or false.
getStoryDetail returns unknown, so narrow it yourself. It carries the same two fields
plus a timeline array of dated updates, newest first and empty when a story has none.
The StoryTimelineEntry type is exported for that array:
import type { StoryTimelineEntry } from "sentisense";
const detail = (await client.documents.getStoryDetail("cluster_abc123")) as {
timeline: StoryTimelineEntry[];
};
for (const update of detail.timeline) {
console.log(new Date(update.publishedAt), update.updateType, update.content);
}client.institutional.getQuarters()
client.institutional.getFlows("2025-02-14", { limit: 20 })
client.institutional.getHolders("AAPL", "2025-02-14")
client.institutional.getActivists("2025-02-14")Paging the holder list. A widely held ticker returns thousands of rows: a megacap quarter is roughly 6,000 holders and 1.5 MB on the wire. Pass limit unless you really want the whole list; omitting the options object sends the original unbounded request, so existing code keeps working.
| Option | Values |
|---|---|
limit |
Maximum rows to return. Must be >= 1; values above 1000 are capped server-side. Omit for the full list. |
offset |
Row offset to start from. Server default is 0. Requires limit. |
sortBy |
"shares" (server default), "valueUsd", or "sharesChangePct". Requires limit. |
sortDir |
"desc" (server default) or "asc". Requires limit. |
limit is the switch for the whole set: send offset, sortBy, or sortDir without it and the server ignores them, returning the full unsorted list with a 200 and no warning.
// Top 10 holders by position value, largest first
const top = await client.institutional.getHolders("AAPL", "2026-03-31", {
limit: 10,
sortBy: "valueUsd",
sortDir: "desc",
});
for (const holder of top.data.holders) {
console.log(holder.filerName, holder.valueUsd);
}
// Walk the list a page at a time
const page = await client.institutional.getHolders("AAPL", "2026-03-31", {
limit: 100,
offset: 100,
});
console.log(`${page.data.holders.length} rows of ${page.data.holderCount}`);A response to a request carrying limit also has three fields the unbounded response does not: returnedCount (rows on this page, smaller than your limit on the last one), offset (echoed back), and notableChanges, a ticker-wide summary of the quarter's biggest position moves so you do not have to scan every page to find them. Each holder row also carries entitySlug, which you can hand straight to institutional.getInstitutionDetail(), and cikCount when the row rolls up several SEC filers under one manager; both are null for filers not matched to an institution page, so check before building a link.
client.politicians.getActivity({ lookbackDays: 90 }) // Market-wide STOCK Act feed
client.politicians.getFilings("NVDA") // Trades in one stock
client.politicians.getMembers() // Tracked members + trade stats
client.politicians.getMember("nancy-pelosi") // One member's profile and trades
client.politicians.getDirectory({ q: "tex" }) // Discover slugs, including former membersPaging the activity feed. A 90-day window is routinely well over a thousand disclosures, and without limit the server returns the first 200 with nothing in the payload to say it stopped. totalCount on the envelope is the real size on every tier, so size the walk from that rather than from data.length.
| Option | Values |
|---|---|
lookbackDays |
Days to look back (1-365). Defaults to 90. |
limit |
Rows to return. Must be >= 1; anything above 500 is capped at 500. Omit for the default 200. |
offset |
Row offset to start from. Defaults to 0. Works with or without limit. |
const first = await client.politicians.getActivity({ limit: 100 });
console.log(`${first.data.length} of ${first.totalCount} disclosures`);
for (let offset = 100; offset < (first.totalCount ?? 0); offset += 100) {
const page = await client.politicians.getActivity({ limit: 100, offset });
for (const trade of page.data) {
console.log(trade.politicianName, trade.ticker, trade.transactionType);
}
}client.insider.getActivity({ lookbackDays: 30 }) // Market-wide buys and sells by ticker
client.insider.getTrades("NVDA", { lookbackDays: 90 }) // Individual filed transactions
client.insider.getClusterBuys({ lookbackDays: 90 }) // 3+ distinct insiders buying the same stockEach trade row carries both the raw SEC transactionCode and a simplified transactionType. Only codes P and S are open-market trades; awards, gifts, exercises, and code F (shares withheld to cover taxes at vest, served as SELL) are corporate mechanics, so read transactionCode when you tally discretionary buying or selling. The market-wide activity endpoint's sells already exclude code F server-side.
The price target cone (mean, high, low, upside %) and consensus are free for everyone with full data. Upgrade/downgrade feeds and forward EPS estimates are limited on free, unlimited on PRO.
client.analyst.consensus("AAPL") // Price target cone + consensus. Free, full data.
client.analyst.actions("AAPL", { lookbackDays: 30 }) // Upgrade/downgrade feed. Free: 3 most recent.
client.analyst.estimates("AAPL") // Forward EPS + surprises. Free: 1 quarter.
client.analyst.marketActivity({ lookbackDays: 7 }) // Market-wide analyst actions (PRO).Coverage answers "who covers this stock and what did they say" in one call, and it is the entry point into the per-analyst surfaces: every named analyst carries the slug that addresses their profile and their calls.
client.analyst.coverage("NVDA", { lookbackDays: 365 }) // Who covers it, by firm. Free: 5 firms.
client.analyst.profile("gil-luria") // One analyst's firms + coverage book.
client.analyst.calls("gil-luria", { limit: 25 }) // Their price target notes, newest first.const { data: book } = await client.analyst.coverage("NVDA");
console.log(`${book.firmCount} firms, ${book.namedAnalystCount} named analysts`);
console.log(`${book.attributedNoteCount} of ${book.noteCount} notes name someone`);
const buckets = book.ratingBuckets;
if (buckets) {
console.log(`${buckets.buy} buy, ${buckets.hold} hold, ${buckets.sell} sell, ${buckets.unrated} unrated of ${buckets.total}`);
}
for (const row of book.coverage.slice(0, 5)) {
if (row.noteCount === 0) {
// A desk can cover a stock on rating actions alone, with no price target.
console.log(`${row.firm}: rating only, ${row.firmRating?.rating}`);
continue;
}
const who = row.latestNote?.analyst ?? "unattributed";
console.log(`${row.firm}: ${row.latestNote?.priceTarget} (${who})`);
for (const analyst of row.analysts) {
if (!analyst.slug) continue;
const calls = await client.analyst.calls(analyst.slug, { limit: 10 });
console.log(` ${analyst.name}: ${calls.totalCount} notes on record`);
}
}Two shapes to read rather than assume. A firm can appear with noteCount: 0, a null latestNote and a populated firmRating, because coverage means a price target or a rating action in the window. And a large, publisher-dependent share of notes name no individual, so an empty analysts array alongside a non-zero noteCount is normal: read attributedNoteCount and unattributedNoteCount off the response rather than hardcoding a rate. The response-level counts survive the free truncation, so they describe the whole window even when only 5 rows come back. An unknown slug throws NotFoundError, which keeps "published nothing we hold" distinguishable from "does not exist".
ratingBuckets sizes the same book by rating tier: buy, hold, sell, unrated and total, counted over every covering firm before the free truncation, so buy + hold + sell + unrated === total and a free key reads the same numbers as a PRO one. unrated is a desk with no current rating on record, such as a price-target-only firm. These count the firms in this coverage book, a different population from the strongBuy through strongSell figures on client.analyst.consensus, which come from the provider's analyst survey. Read one or the other, do not reconcile them.
The earnings analysis report is the assembled version of a quarter: one object per fiscal period carrying the editorial headline, the KPI cards with year-over-year deltas, the guidance language as management phrased it, and a summary of the earnings call. Pair it with the recent-reporters feed to drive a post-earnings sweep. Both return the preview envelope.
client.earnings.getSummaries("AAPL", { limit: 4 }) // Per-quarter analysis, newest first. Free: latest quarter, shaped.
client.earnings.getRecent({ days: 7, limit: 25 }) // Who reported in the last N days. Full window on every key.const res = await client.earnings.getSummaries("AAPL", { limit: 1 });
const quarter = res.data[0];
if (quarter) {
console.log(quarter.fiscalPeriod, quarter.reportDate);
console.log(quarter.headline);
for (const kpi of quarter.kpiHighlights ?? []) {
console.log(` ${kpi.label}: ${kpi.value} (${kpi.yoy ?? "no YoY"})`);
}
if (res.isPreview) {
// Free key: section titles stand in for the bodies.
console.log("Summary covers:", quarter.summaryTopics?.join(", "));
} else {
console.log(quarter.summaryMd);
}
}The forward-looking half of the family is client.calendar.getEarnings(), which covers scheduled dates and consensus EPS rather than results.
client.stocks.getKpis("AAPL") // Product metrics and segment revenue. Free: metadata only. PRO: full series.
client.stocks.listKpiCoverage() // All tickers with curated KPI data (free, no quota cost)Composition data is public; the holdings-weighted aggregate views follow the same PRO-with-preview pattern as analyst and insider data. Aggregates synthesize fund-level views from each constituent's per-stock data, weighted by allocation, with a coverage block on every response.
client.etfs.list() // Every ETF tracked
client.etfs.holdings("QQQ") // Full composition + freshness metadata
client.etfs.analystAggregate("QQQ") // Holdings-weighted analyst consensus
client.etfs.insiderAggregate("ARKK", { lookbackDays: 90 }) // Holdings-weighted Form 4 net flow
client.etfs.sentimentAggregate("QQQ") // Constituent-weighted vs direct Score// Time-series metrics (v2 API)
client.entityMetrics.getMetrics("AAPL", { metricType: "sentiment" })
client.entityMetrics.getMetrics("AAPL", {
metricType: "mentions",
startTime: Date.now() - 7 * 86400000,
endTime: Date.now(),
maxDataPoints: 100,
})
// Distribution by source
client.entityMetrics.getDistribution("AAPL", "sentiment")
client.entityMetrics.getDistribution("AAPL", "mentions", { dimension: "source" })Available metric types: mentions, sentiment, sentisense_score, sentisense_rating, social_dominance, creators. sentisense_rating is the SentiSense Rating score and is a time series only: it has no source breakdown, so getDistribution answers with an empty distribution for it.
End-of-day options positioning: where implied volatility, put/call flow and skew are unusual today, and how a name's readings have trended.
client.options.getOverview() // Market-wide radar, ranked
client.stocks.getOptionsSummary("NVDA") // One name's full dossier
client.stocks.getOptionsHistory("NVDA", { window: "2y" }) // That name's daily seriesThe radar carries two separately-ranked boards: data.rows for stocks and data.etfRows for ETFs. Keep them apart. Every reading behind a row's interestScore is a percentile of that ticker's own trailing history, so a ranking built across both boards compares numbers measured against different baselines. The aggregates split the same way, with the etf-prefixed fields describing the ETF board alone.
A row whose baseline is still building carries its raw readings with the percentiles and interestScore omitted, which means "not enough history yet" rather than "nothing interesting". getOptionsSummary reports an uncovered ticker as a null payload inside the usual envelope, so the check is result.data === null: the response object itself is always truthy, and a bare if (summary === null) never fires. getOptionsHistory reports it as an empty series instead, so check the array's length rather than null-checking there.
Where a stock ranks against the other stocks rated that day, as a score, a letter and a percentile, plus the six dimensions the rank is blended from. It is a relative research signal for informational and educational purposes, not financial, investment or trading advice, and not a recommendation about any security. Every response carries the wording to display alongside a grade in disclaimer. Methodology.
const rating = await client.stocks.getRating("AAPL");
if (rating.rated) {
console.log(rating.letter, rating.score, "percentile", rating.percentile, "of", rating.ratedCount);
for (const adj of rating.riskAdjustments ?? []) console.log(" ", adj.condition, -adj.points);
for (const dim of rating.dimensions.filter((d) => d.present)) {
console.log(" ", dim.label, dim.percentile);
}
} else {
console.log("no grade today:", rating.reason);
}getRating returns StockRatingResponse, a discriminated union on rated of StockRating (graded) and StockNotRated. The if narrows to score, letter, percentile, composite, ratedCount and methodologyVersion, the else to reason, dimensionsPresent and presentDimensions. Branch on that flag rather than testing a field for undefined.
Having no grade is a normal 200, not a 404: ETFs and tickers outside the swept universe answer that way, and reason is one of stale, not_rated_today, insufficient_dimensions or insufficient_coverage_weight. Only a ticker that resolves to nothing we track rejects with NotFoundError.
dimensions always holds all six rows in a fixed order, including the ones with no data, which arrive with present false and a null percentile. Read present first and never substitute zero for a missing percentile: zero is the bottom of the cross-section, absence is not a position on it. Only the smart-money dimension carries subLegs.
score is not percentile. percentile is the rank of the blended signals against the day's rated set. score = percentile - sum(riskAdjustments.map((a) => a.points)), floored at 10 when fewer than five dimensions are available, and it is the number letter bands (A 90, B 70, C 30, D 10). bucketLetter is the band the percentile alone would give, so the two letters differ by exactly what the conditions cost. riskAdjustments itemises that cost, penaltyPoints totals it, and riskConditions names the active ones from the RiskCondition union: thin_coverage, weak_dimension, unprofitable, no_fundamentals, high_leverage, unseasoned_listing, small_market_cap, thin_liquidity, extended_price, insider_selling and institutional_outflow.
All five are optional: a response served before they shipped omits them. For the daily history of a stock's score, ask entityMetrics.getMetrics for the sentisense_rating metric.
client.marketMood.get() // Composite market sentiment with sub-signals
client.kb.getPopularEntities() // Most-tracked entities
client.kb.searchEntities("Tesla") // Resolve a name, alias, ticker or slug to what we trackEntity search is resolution, not enumeration: the query must be at least 2 characters, the match count is capped at 25, and it returns a bare EntitySearchResult[] rather than a PreviewResponse envelope. Each hit carries name, type, the ticker for a listed entity (null for everything else), and the urlSlug the metric endpoints address that entity by, which is the only way to get a handle for a person, product or topic with no ticker.
const hits = await client.kb.searchEntities("Tesla", { type: "company", limit: 5 });
const symbol = hits.find((hit) => hit.ticker)?.ticker; // "TSLA"An empty array is the normal answer for a query that matches nothing, so branch on length rather than catching.
Filter the tracked universe on the SentiSense Score, attention, analyst consensus, technicals and price in one query. Screening on analyst ratings alone is something a dozen free tools do; screening on analyst ratings where the Score disagrees is not.
client.screener.fields() // Every filterable field, both universes, with units + operators
client.screener.screens() // The curated screens shipped in the product, each with a runnable plan
client.screener.run({ plan, tickers, limit }) // Run a screen against the stock universe
client.screener.runEtfs({ plan, limit }) // Run a screen against the ETF universe// Run a curated screen as-is
const { screens } = await client.screener.screens();
const crowdVsStreet = screens.find((s) => s.id === "crowd-vs-street")!;
const curated = await client.screener.run({ plan: crowdVsStreet.plan, limit: 25 });
console.log(`${curated.matched} matched, showing ${curated.results.length}`);
// Or build your own: bullish Score, thin analyst enthusiasm
const res = await client.screener.run({
plan: {
filters: [
{ fieldName: "SENTI_SCORE_7D", op: "GTE", value: 13 },
{ fieldName: "ANALYST_BUY_RATIO_PCT", op: "LTE", value: 30 },
{ fieldName: "ANALYST_COUNT", op: "GTE", value: 5 },
],
sort: { fieldName: "SENTI_SCORE_7D", dir: "DESC" },
},
limit: 25,
});
for (const row of res.results) {
console.log(row.ticker, row.sentiSenseScore7D, row.analystBuyRatioPct);
}limit rides next to the plan rather than inside it, because a plan is a stored object and paging is a transport concern. It defaults to 100 and caps at 500. matched is the count before limit was applied, so truncation is visible. tickers is optional: omit it to screen the whole tracked universe, pass a list to screen a watchlist.
Three field semantics are worth stating outright, because guessing them wrong produces a screen that looks fine and means nothing:
ANALYST_RATING_MEANis inverted. It is the vendor's 1-to-5 scale where 1.0 is strong buy, so bullish isLTE 2.5. PreferANALYST_BUY_RATIO_PCT, which runs the intuitive direction.MA_CROSS_STATEis ordinal, not a percentage:1golden cross,-1death cross,0neither. UseEQ.SENTIMENT_DIRECTIONis the sign of the 7-day SentiSense Score (1/0/-1) with a neutral band of plus-or-minus 5. Despite the name it is not sentiment polarity.
The Score fields (SENTI_SCORE_7D, SENTI_SCORE_1M, SCORE_CHANGE_7D) are the SentiSense Score, not polarity: unbounded, banded at 5 / 13 / 23 either side of zero. Filter on those band edges, not on values like 0.5, which behave as "any positive score". Nulls never match in either direction, so RETURN_1Y >= 0 and RETURN_1Y < 0 do not partition the universe: a stock listed four months ago is in neither result. If a screen returns fewer rows than you expect, check coverage before you check your thresholds.
On the ETF side, CONSTITUENTS_WEIGHTED_SENTISENSE is the holdings-weighted Score across what the fund owns and is usually the one you want; DIRECT_SENTISENSE is the Score from chatter about the fund ticker itself. WEIGHT_COVERED_PCT tells you how much of the fund's weight had constituent data behind the weighted number.
Screens read a snapshot that refreshes every 20 minutes, so this is not a quote feed. Use client.stocks.getQuote() for current quotes.
import SentiSense, { AuthenticationError, RateLimitError } from "sentisense";
try {
const summary = await client.stocks.getAISummary("AAPL");
} catch (error) {
if (error instanceof AuthenticationError) {
// 401 or 403: invalid/missing API key or insufficient tier
} else if (error instanceof RateLimitError) {
// 429: quota exceeded
}
}| Error class | HTTP status | When |
|---|---|---|
AuthenticationError |
401, 403 | Invalid API key or insufficient tier |
NotFoundError |
404 | Resource not found |
RateLimitError |
429 | Quota exceeded |
APIError |
Other 4xx/5xx | General API error |
All errors extend SentiSenseError and include status, code, and message properties.
- Get a free API key: app.sentisense.ai/get-api-key
- API documentation: sentisense.ai/docs/api
- Changelog: CHANGELOG.md
SentiSense provides research data for informational and educational purposes, not investment advice.
MIT