SentiSense Rating API
The daily 0 to 100 score and A to F letter grade for a US stock: the rank of a seven-dimension blend of crowd sentiment, smart money, options positioning, analysts, fundamentals, earnings and technicals, less its graded risk adjustments, with the full composition behind it.
Overview
The SentiSense Rating is a letter grade, A to F, that says where one stock stands today against every other stock we rate. It blends seven independent signal families: crowd sentiment, smart money, options positioning, analysts, fundamentals, earnings and technicals. The same grade drives the Rating card in the SentiSense app.
The score is a sum of two documented constituents.
score = percentile - sum(riskAdjustments[].points)
each adjustment worth up to 12 points, most of them graded
floored at 10 when fewer than 6 of the 7 dimensions are available, and at 0 otherwise
percentile is the rank: 0 to 100, how the stock's blended signals compare with the rest of the rated universe that day. riskAdjustments pairs each condition with what it actually cost, and penaltyPoints is their sum, to one decimal. score is the result, 0 to 100 with one decimal, and it is the headline: letter is a band of score, so the two can never disagree. All of them are returned side by side so you can check the arithmetic yourself.
Most adjustments are graded, so do not assume a flat charge. Seven of the eleven conditions scale with how far past the threshold the stock sits, up to 12 points; four are flat 12s because they are genuinely binary. Read the cost from riskAdjustments[].points, never from 12 x riskConditions.length.
Report the score and the letter; the percentile is the rank before the adjustments. A response reading "percentile": 100.0, "penaltyPoints": 14.4, "score": 85.6, "letter": "B" is correct and internally consistent. Describe it as "B, 85.6 out of 100, ranked in the 100th percentile with 14.4 points of risk adjustments", not as a data error and not by quoting the rank as though it were the grade. bucketLetter is the band the rank alone would land in, available for callers who want it.
What it is not. The Rating is not a recommendation, a price target, a forecast of returns, or a statement that a stock suits anyone. It describes where a stock sits today across public signals. When you render it or summarize it, describe it as a score and a rank ("B, 85.6 out of 100, ranked in the 80th percentile of rated stocks today"). Do not translate a letter into buy, sell, hold, long, short or avoid: those are trade instructions and the grade does not carry that meaning.
The Rating is not the SentiSense Score. The Score is the continuous crowd-sentiment number for a stock, and it is one of the seven inputs here. The Rating is the composite grade over all seven. Those are the only two headline measures we publish, and they are different objects with different scales.
The letter bands, on the score:
| Letter | Score |
|---|---|
A |
90 and above |
B |
70 to 89.9 |
C |
30 to 69.9 |
D |
10 to 29.9 |
F |
Below 10 |
The bands are not fixed shares of the market: a letter is whatever band the score lands in. At most one stock in ten can carry an A, since only the top tenth of ranks reaches 90 before any adjustment, and in practice far fewer do.
The eleven risk conditions. Each is worth up to 12 points, several can apply at once, and each is a value in the riskConditions array:
riskConditions value |
When it applies | Points |
|---|---|---|
thin_coverage |
Thin coverage: currently 6 points for each missing dimension once fewer than 6 of the 7 are available, so 5 dimensions cost 6 and 4 cost 12, while 6 or 7 cost nothing. Below 6 the score is also floored at 10 | up to 12, graded |
insider_selling |
The clustered_insider_selling flag is active: currently 5 or more distinct insiders filing open-market sales in the trailing 90 days, none under a scheduled 10b5-1 plan, and no insider purchasing in the same window |
12 |
institutional_outflow |
The institutional_outflow_against_price flag is active, scaled by how deep in the bottom quarter of net institutional change the stock sits |
up to 12, graded |
unprofitable |
Trailing twelve month operating margin or net margin is not positive | 12 |
no_fundamentals |
No filed financials, so the profitability and leverage conditions cannot be evaluated | 12 |
high_leverage |
Leverage above the current threshold: 6 points per unit of debt to equity above 2 | up to 12, graded |
unseasoned_listing |
Unseasoned listing: currently fewer than 250 trading sessions in the past 400 days. Counted in sessions rather than from a listing date, because there is no reliable listing date behind the grade | 12 |
weak_dimension |
The weakest available dimension sits below the 20th percentile, currently 0.6 points per percentile below 20. | up to 12, graded |
small_market_cap |
Small company: market capitalization currently under 2 billion dollars, scaled by the shortfall | up to 12, graded |
thin_liquidity |
Thinly traded: dollar volume in the latest session currently under 10 million dollars, scaled by the shortfall | up to 12, graded |
extended_price |
Extended price: currently within 5 percent of the 52-week high and more than 25 percent above the 200-day moving average, scaled by the excess above 25 percent | up to 12, graded |
unusual_options_flow is informational and costs nothing.
Condition thresholds, and the rate at which a graded one accrues, are tuned as calibration data accumulates and may change without a version bump; the constituents, their maximum and the formula do not change without one. The eleven names above are a closed vocabulary: treat an unrecognized value as a condition this client does not know about, costing whatever riskAdjustments says it cost, rather than as an error.
The seven dimensions and their weights:
| Dimension key | Label | Weight | What it reads |
|---|---|---|---|
crowd |
Crowd sentiment | 16% | The 7-day SentiSense Score (70%) and 7-day social dominance (30%) |
smart_money |
Smart money | 16% | Institutional 13F net change, insider flow balance, congressional flow balance |
options |
Options positioning | 12% | The options-implied sentiment reading |
analysts |
Analysts | 12% | Share of favorable analyst ratings |
fundamentals |
Fundamentals | 20% | Operating margin, return on equity, revenue growth |
earnings |
Earnings | 8% | Latest EPS surprise |
technicals |
Technicals | 16% | Where the price sits versus its own history: distance from its 200-day and 50-day averages, its twelve-month path, and how calm or violent its recent sessions have been |
Each dimension is turned into a percentile among the rated stocks that have it that day, then blended with those fixed weights. The weights are engineering priors ordered by freshness and by how much independent information each family carries. They are not the output of a return backtest, and a later version may revise them. The full derivation, including every availability floor and the sub-signal weights inside smart money, is published at sentisense.ai/methodology.
A missing dimension counts as the median. An unavailable dimension is scored at the 50th percentile at its full weight rather than dropped, so thin coverage pulls a grade toward the middle instead of letting two strong readings carry a stock to the top. A stock is rated only when at least 4 of the 7 dimensions are available and those dimensions carry at least 50 percent of the total weight. Otherwise the response is an ordinary 200 with rated: false. A stock is never given a C by default, so treat "not rated" as an absence of evidence, not a neutral grade. This is why smaller companies with thin coverage, no options market and no analyst following are often unrated.
Freshness: the Rating is recomputed once per trading day, at 07:15 Eastern Time on the morning after each trading session, which puts the run on Tuesday through Saturday. It reads the prior session's crowd sentiment, the options reading computed that morning from the prior session's chain, the prior session's closing prices, and the latest available filings, consensus and earnings data. asOf on the response is the date of the run. Polling faster than daily returns the same values, so cache on asOf.
Access: an API key is required on every call, and each request counts against your monthly quota (Free: 1,000 requests/month; PRO: no monthly cap) as well as your per-minute rate limit. There is no PRO gate on the data itself: every field below is served in full on the Free tier. Anonymous calls return 401 api_key_required.
GET /{ticker}
Today's Rating for one stock: the score, the letter that bands it, the rank and adjustments it was built from, the composite, and the per-dimension composition behind it.
Authentication: API key required
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
ticker |
path | Yes | Stock ticker (e.g. AAPL). Case-insensitive; dual-class symbols resolve to their canonical form |
Example Request:
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/rating/AAPL"
Response (rated): a flat object, no {isPreview, data} wrapper.
| Field | Type | Description |
|---|---|---|
ticker |
string | The resolved ticker |
kbEntityId |
string | The ontology entity this ticker resolved to, usable on the Metrics and Entities APIs |
rated |
boolean | true on this shape |
score |
number | The headline. 0 to 100 with one decimal: percentile minus penaltyPoints, floored at 10 on thin coverage and at 0 otherwise |
letter |
string | A, B, C, D or F. The band score falls in, and the one to display beside it |
percentile |
number | 0 to 100, the stock's rank among the stocks rated today. This is what score was built from, before the risk adjustments |
penaltyPoints |
number | What the risk adjustments took off, to one decimal: the sum of riskAdjustments[].points, so percentile - penaltyPoints reproduces score up to the floor |
riskAdjustments |
array of object | { condition, points } per adjustment, points to one decimal. This is where the cost of each condition lives, and it is not a constant: most adjustments are graded |
riskConditions |
array of string | The names in riskAdjustments that cost more than zero, from the closed vocabulary above. Empty when the stock carries none, in which case score equals percentile |
bucketLetter |
string | The band percentile alone would fall in. Equal to letter when the stock carries no risk conditions |
composite |
number | The weighted blend before it was ranked. Comparable within a day only; read percentile for anything cross-sectional |
ratedCount |
integer | How many stocks were rated in this run, the denominator behind percentile |
asOf |
ISO date | The run date |
methodologyVersion |
string | The methodology in force, e.g. 2026.09-v3. Changes to weights, floors or letter edges get a new version |
dimensions |
array | Always all seven rows, in a fixed order, each with a present flag |
flags |
array | Anomaly flags that were evaluable for this stock, each with an active boolean |
disclaimer |
string | The disclaimer to render alongside the grade |
Each element of dimensions:
| Field | Type | Description |
|---|---|---|
key |
string | crowd, smart_money, options, analysts, fundamentals, earnings, technicals |
label |
string | Display label for the dimension |
percentile |
number or null | The dimension's percentile among rated stocks that have it. null when present is false |
raw |
number or null | The underlying reading on its own scale. null on smart_money, which has no single raw number, and null when present is false |
rawLabel |
string or null | What raw measures, e.g. Operating margin, percent |
present |
boolean | Whether this dimension contributed. Branch on this, not on raw != null |
subLegs |
array | smart_money only: { key, label, raw, unit } for inst_13f, insider and congress. A leg with no activity in its window carries raw: null and was left out of the blend |
Each element of flags is { key, label, active }, over clustered_insider_selling, institutional_outflow_against_price and unusual_options_flow. The first two are also risk conditions and take points off the score while active, clustered_insider_selling a flat 12 and institutional_outflow_against_price a graded amount; unusual_options_flow is informational. The array holds only the flags that could be evaluated for this stock, so a flag missing from the array means "not assessable here", which is not the same as active: false. Look flags up by key rather than by position.
Checking the arithmetic. In the example below the stock ranked at the 100th percentile and carries two risk conditions with different costs, a graded 2.4 and a flat 12.0, summing to the 14.4 in penaltyPoints; the score is 85.6, which bands to B. bucketLetter reports the A the rank alone would have given. Render the numbers together ("B, 85.6 out of 100, ranked in the 100th percentile with 14.4 points of risk adjustments") rather than quoting any one of them as the grade.
{
"ticker": "AAPL",
"kbEntityId": "kb/company/1",
"rated": true,
"score": 85.6,
"letter": "B",
"percentile": 100.0,
"penaltyPoints": 14.4,
"riskAdjustments": [
{ "condition": "weak_dimension", "points": 2.4 },
{ "condition": "high_leverage", "points": 12.0 }
],
"riskConditions": ["weak_dimension", "high_leverage"],
"bucketLetter": "A",
"composite": 0.19,
"ratedCount": 1000,
"asOf": "2026-09-03",
"methodologyVersion": "2026.09-v3",
"dimensions": [
{
"key": "crowd",
"label": "Crowd sentiment",
"percentile": 84.0,
"raw": 9.2,
"rawLabel": "7-day SentiSense Score",
"present": true
},
{
"key": "smart_money",
"label": "Smart money",
"percentile": 45.0,
"raw": null,
"rawLabel": null,
"present": true,
"subLegs": [
{ "key": "inst_13f", "label": "13F net change", "raw": 4.0, "unit": "%" },
{ "key": "insider", "label": "Insider flow balance", "raw": -1.0, "unit": "ratio" },
{ "key": "congress", "label": "Congress flow balance", "raw": -0.4, "unit": "ratio" }
]
},
{
"key": "options",
"label": "Options positioning",
"percentile": 68.0,
"raw": 0.18,
"rawLabel": "Options sentiment",
"present": true
},
{
"key": "analysts",
"label": "Analysts",
"percentile": 30.0,
"raw": 57.0,
"rawLabel": "Share of favorable analyst ratings",
"present": true
},
{
"key": "fundamentals",
"label": "Fundamentals",
"percentile": 95.0,
"raw": 33.2,
"rawLabel": "Operating margin, percent",
"present": true
},
{
"key": "earnings",
"label": "Earnings",
"percentile": 38.0,
"raw": 0.07,
"rawLabel": "Latest EPS surprise, percent",
"present": true
},
{
"key": "technicals",
"label": "Technicals",
"percentile": 61.0,
"raw": 7.9,
"rawLabel": "Distance from the 200-day average, percent",
"present": true
}
],
"flags": [
{ "key": "clustered_insider_selling", "label": "Clustered insider selling", "active": false },
{ "key": "institutional_outflow_against_price", "label": "Institutional outflows against price", "active": false },
{ "key": "unusual_options_flow", "label": "Unusual options flow", "active": true }
],
"disclaimer": "The SentiSense Rating is a relative, automatically generated research signal for informational and educational purposes only. ..."
}
When a stock is not rated
A covered ticker that misses the coverage floor returns 200 with rated: false. This is a normal state, not an error, and it is common: index and sector ETFs are never rated, and thinly covered small caps often are not.
| Field | Type | Description |
|---|---|---|
ticker / kbEntityId / asOf |
Same as the rated shape | |
rated |
boolean | false |
reason |
string | not_rated_today, insufficient_dimensions or insufficient_coverage_weight |
presentDimensions |
array | The dimension keys that did have data, e.g. ["crowd", "analysts"]. Empty when none did |
dimensionsPresent |
integer | How many of the seven had data. A separate field from presentDimensions, and the one that is optional: it is omitted when the run holds no record for this ticker at all. Read the length of presentDimensions if you need a count you can always rely on |
dimensions |
array | Still all seven rows, with present false on the missing ones, so a composition UI can render the gaps |
flags |
array | Empty |
disclaimer |
string | Same text as the rated shape |
score, letter, percentile, penaltyPoints, riskAdjustments, riskConditions, bucketLetter, composite, ratedCount and methodologyVersion are absent entirely on this shape. Branch on rated before reading any of them.
presentDimensions and dimensionsPresent are two different fields whose names are one transposition apart: the first is the list of keys, the second the integer count. A stock the run looked at and refused carries both, for example "reason": "insufficient_dimensions" with "dimensionsPresent": 2 and "presentDimensions": ["crowd", "analysts"]. A stock the run never reached carries not_rated_today with an empty list and no count at all, which is the shape below.
{
"ticker": "SPY",
"kbEntityId": "kb/etf/3",
"rated": false,
"asOf": "2026-09-03",
"reason": "not_rated_today",
"presentDimensions": [],
"dimensions": [
{
"key": "crowd",
"label": "Crowd sentiment",
"percentile": null,
"raw": null,
"rawLabel": "7-day SentiSense Score",
"present": false
}
],
"flags": [],
"disclaimer": "The SentiSense Rating is a relative, automatically generated research signal for informational and educational purposes only. ..."
}
An identifier that resolves to no known stock returns 404 entity_not_found with a hint pointing at GET /api/v1/kb/entities/search?q= on the Entities API. That is a different answer from rated: false: one means "we do not know this ticker", the other means "we know it and did not grade it today".
From the SDKs
Both SDKs wrap the endpoint and return the same fields. The letter is the band of score; percentile is the rank before the risk adjustments.
Python (sentisense 0.49.0 or later):
from sentisense import SentiSenseClient
client = SentiSenseClient(api_key="YOUR_KEY")
rating = client.get_rating("AAPL")
if rating.rated:
print(rating.letter, rating.score, rating.percentile)
for adj in rating.riskAdjustments:
print(adj.condition, adj.points)
Node (npm package sentisense 0.51.0 or later):
import SentiSense from "sentisense";
const client = new SentiSense({ apiKey: process.env.SENTISENSE_API_KEY });
const r = await client.stocks.getRating("AAPL");
if (r.rated) {
console.log(r.letter, r.score, r.percentile);
for (const adj of r.riskAdjustments ?? []) console.log(adj.condition, adj.points);
}
Rating history
Each daily run is stored as a point on the sentisense_rating metric, so the Metrics API serves the history with the same call shape as every other metric:
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
"https://app.sentisense.ai/api/v2/metrics/entity/AAPL/metric/sentisense_rating?startTime=1785900000000"
The flat value on each point is the score, 0 to 100 with one decimal (the number the letter is derived from), not the letter, not the percentile and not the composite. The letter, the composite and every per-dimension figure ride along under metricValue.properties: letter, composite, ratedCount, coverageWeight, dimensionsPresent, methodologyVersion, a dim<Name>Pct and dim<Name>Raw pair per dimension, the three sm* smart-money legs, and a flag<Name> boolean per flag.
Points appear only for days the stock was rated, so a gap in the series means "not rated that day" rather than a missing reading. The series starts at the first published run and deepens by one point per trading day, so treat it as short until it has accumulated. Because the grade is a rank, its history stays comparable even when the weights are revised.
Screening on the Rating
The Screener API exposes the letter as a filterable field, so you can put the grade in the same WHERE clause as the SentiSense Score, analyst consensus and technicals.
SENTISENSE_RATING is a string field over A, B, C, D, F and accepts IN and NOT_IN only. It is sortable, and sorting resolves to the underlying score rather than the letter's alphabetical order, so the ordering inside a letter is defined and DESC puts the highest score first. Unrated stocks match no letter and are excluded in both directions, the same null rule as every other field.
curl -X POST https://app.sentisense.ai/api/v1/screener/execute \
-H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"plan": {
"filters": [
{ "fieldName": "SENTISENSE_RATING", "op": "IN", "values": ["A", "B"] },
{ "fieldName": "ANALYST_BUY_RATIO_PCT", "op": "LTE", "value": 40 }
],
"sort": { "fieldName": "SENTISENSE_RATING", "dir": "DESC" }
},
"limit": 25
}'
Matching rows carry ratingLetter, ratingScore and ratingPercentile alongside the other screener columns, so one screen returns the grade, the score behind it and the rank it started from without a second call. The /stocks table sorts its Rating column on ratingScore for the same reason.
Errors
| Status | Code | Description |
|---|---|---|
| 200 | - | Success. rated: false when the stock missed the coverage floor for this run; this is a normal state, not an error |
| 401 | api_key_required | No API key on the call |
| 404 | entity_not_found | The identifier matches no known stock. The message points at GET /api/v1/kb/entities/search?q= |
| 429 | rate_limit_exceeded | Per-minute rate limit exceeded (Free: 30/min, PRO: 300/min) |
| 429 | quota_exceeded | Monthly request quota exhausted (Free: 1,000/month; PRO has no monthly cap) |
Educational use only
The SentiSense Rating is a relative, automatically generated research signal provided for informational and educational purposes. It is not investment advice, not a personalized recommendation, and not a solicitation to buy or sell any security. Data may be delayed, incomplete or inaccurate, and the weights behind the grade have not been validated against forward returns. Verify with official sources and consult a qualified financial professional before making any investment decision. Use of the API is subject to the API Terms of Service.
Try It
Test endpoints directly from your browser. Paste your API key once: it's saved locally and shared across all widgets. Get a free key
GET/api/v1/rating/{ticker}
Today's score, letter and seven-dimension composition for a stock
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/rating/AAPL"