ETFs API
ETF discovery, holdings composition, and holdings-weighted aggregate views (analyst consensus, insider activity, sentiment). Read fund-level signals derived from each constituent's per-stock data, weighted by allocation.
Overview
The ETFs API exposes fund-level data for the exchange-traded funds SentiSense tracks. Two flavors of endpoints:
- Composition data (
/,/{ticker}/holdings) — issuer-level facts: which ETFs exist, what each one holds, when the composition was last refreshed. - Holdings-weighted aggregates (
/{ticker}/aggregates/...) — synthesized fund-level views derived from each constituent's per-stock data, weighted by allocation. Three rollups today: analyst consensus, insider activity, and SentiSense sentiment.
Why aggregates matter: funds aren't rated by sell-side analysts, don't have insiders of their own, and may not get many direct news mentions. But the companies inside a fund do. The aggregate endpoints surface "what the underlying constituents are signaling, weighted by how much of the fund they represent" — a view that's genuinely useful for ETF research and isn't published anywhere else.
Coverage transparency: every aggregate response carries a coverage block (holdingsCount, holdingsCovered, weightCovered, partial, totalKnownHoldings) so consumers can interpret the headline number with the right confidence. When fewer than ~30% of fund AUM has the underlying data (e.g. foreign-listed funds like VEA/VWO where most holdings have no per-stock coverage), the endpoint returns 404 rather than publish a misleading aggregate.
Beta notice: ETF coverage is in beta as of 2026-05-15. Starting with a limited set of widely-traded funds (SPY, QQQ, IWM, VOO, VTI, the major SPDR sector ETFs, etc.). Expect coverage and aggregate freshness to improve over time.
Access: all endpoints require an API key. The composition endpoints (list, holdings) are on the Free tier and the list endpoint does not consume monthly quota. The aggregate endpoints follow the standard PRO-with-FREE-preview pattern (FREE sees the headline + coverage block, PRO unlocks per-holding contributors).
GET /
Returns every ETF SentiSense tracks. Sorted by ticker for stable client-side caching.
Authentication: API key required (no quota cost) | Parameters: None
Example Request:
curl "https://app.sentisense.ai/api/v1/etfs"
Response: array of ETF info objects.
| Field | Type | Description |
|---|---|---|
ticker |
string | Fund ticker (e.g. QQQ) |
name |
string | Official fund name |
kbEntityId |
string | Knowledge base entity ID (e.g. kb/etf/22) |
urlSlug |
string | URL-friendly slug for the fund detail page |
issuer |
string | Fund issuer (e.g. Invesco, Vanguard, iShares) |
trackedIndex |
string | null | Reference index or strategy description |
assetClass |
string | null | One of Equity, Bond, Commodity, etc. |
GET /{ticker}/holdings
Returns the full composition of an ETF: which stocks it holds, each holding's weight, and freshness metadata.
Authentication: API key required
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
ticker |
path | Yes | - | ETF ticker (e.g. QQQ) |
Example Request:
curl "https://app.sentisense.ai/api/v1/etfs/QQQ/holdings"
Response object:
| Field | Type | Description |
|---|---|---|
ticker |
string | Fund ticker |
issuer |
string | Fund issuer |
issuerEndpoint |
string | null | Source URL the composition was fetched from |
asOfDate |
string | Composition snapshot date from the issuer (ISO date YYYY-MM-DD) |
fetchedAt |
long | null | When SentiSense refreshed the composition (epoch seconds) |
nextRefreshDue |
string | When the composition is scheduled to be refreshed next (ISO date YYYY-MM-DD) |
totalHoldings |
int | Number of holdings in the visible composition |
holdings |
array | Per-holding rows (see below) |
partial |
boolean | null | true when the composition is a top-N view rather than the full fund |
totalKnownHoldings |
int | null | Issuer's reported total holdings when partial=true |
Holding object:
| Field | Type | Description |
|---|---|---|
ticker |
string | Constituent ticker |
name |
string | Constituent company name |
weightPct |
decimal | Holding weight in the fund, expressed as a percentage (0–100) |
firstSeen |
string | null | First date this holding appeared in the composition (ISO date YYYY-MM-DD) |
Returns 404 if the ETF isn't tracked or no holdings file is available (commodity-only funds like GLD have no equity holdings).
GET /{ticker}/quote
Returns a single-call aggregate snapshot for the ETF detail page: live price + today's OHLC + 52-week range + trailing-12-month dividend yield + ETF fundamentals (AUM, expense ratio, NAV, inception date). Peer of GET /stocks/{ticker}/quote for fund tickers.
Authentication: API key required | Rate limit: standard quota
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
ticker |
path | Yes | - | ETF ticker (e.g. VTI) |
Example Request:
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/VTI/quote"
Response object:
| Field | Type | Description |
|---|---|---|
ticker |
string | ETF ticker |
currentPrice |
number | null | Regular-session price. During RTH (09:30 to 16:00 ET): live last trade. Otherwise: most recent regular-session close. |
change |
number | null | currentPrice change vs previousClose |
changePercent |
number | null | currentPrice change percentage vs previousClose |
volume |
number | null | Volume for the current session |
open |
number | null | Opening price for the current session |
dayHigh |
number | null | Intraday high |
dayLow |
number | null | Intraday low |
previousClose |
number | null | Previous session close |
week52High |
number | null | 52-week high |
week52Low |
number | null | 52-week low |
dividendYield |
number | null | Trailing-12-month distribution yield (decimal, e.g. 0.0104 for 1.04%) |
aum |
number | null | Assets under management, USD. ETF analogue of marketCap on the stock quote. |
expenseRatio |
number | null | Annual fund expense ratio (decimal, e.g. 0.0003 for 0.03%) |
nav |
number | null | Net asset value per share, USD |
inceptionDate |
string | null | Fund inception date (ISO YYYY-MM-DD) |
timestamp |
number | null | Quote timestamp (epoch milliseconds) |
extendedHours |
object | null | Extended-hours view (pre-market or after-hours). Absent during RTH, overnight, and weekends. Shape: { session, price, change, changePercent }. |
All fields except ticker are nullable. Render "--" or hide the row when a field is absent.
Example response:
{
"ticker": "VTI",
"currentPrice": 363.74,
"change": -4.66,
"changePercent": -1.27,
"volume": 3500000,
"open": 364.53,
"dayHigh": 364.93,
"dayLow": 362.28,
"previousClose": 367.40,
"week52High": 368.25,
"week52Low": 283.00,
"dividendYield": 0.0104,
"aum": 2200000000000,
"expenseRatio": 0.0003,
"nav": 362.68,
"inceptionDate": "2001-05-24",
"timestamp": 1747535400000
}
Stock tickers: This endpoint is ETF-only. Calling it with a stock ticker (e.g. AAPL) returns 400 ticker_is_not_etf with a pointer to GET /stocks/{ticker}/quote which returns market cap, P/E, and EPS instead of AUM and expense ratio.
Rate-limit note: Cached for 15 seconds server-side. Each call still counts toward your monthly quota.
GET /{ticker}/aggregates/analyst
Returns a holdings-weighted analyst consensus for an ETF, derived from each covered constituent's per-stock analyst coverage. Math: weight × per-stock upside, renormalized to the covered subset.
Authentication: API key required. Returns the full response (including topContributors) to every API caller; PRO and FREE differ only in per-tier rate limits and monthly quota.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
ticker |
path | Yes | - | ETF ticker |
Example Request:
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/QQQ/aggregates/analyst"
from sentisense import SentiSenseClient
client = SentiSenseClient(api_key="ss_live_YOUR_KEY")
result = client.get_etf_analyst_aggregate("QQQ")
agg = result["data"]
print(f"QQQ weighted 12mo upside: {agg['weightedConsensus']['upsidePercent']}%")
print(f"Coverage: {agg['coverage']['holdingsCovered']} of {agg['coverage']['holdingsCount']} holdings"
f" ({agg['coverage']['weightCovered']}% AUM)")
Response Schema:
| Field | Type | Description |
|---|---|---|
isPreview |
boolean | true when the caller is on the FREE tier |
previewReason |
string | "PRO_REQUIRED" or null |
data |
object | Aggregate (see below) |
Aggregate object:
| Field | Type | Description |
|---|---|---|
ticker |
string | ETF ticker |
asOfDate |
string | Composition snapshot date the rollup was computed against (ISO date YYYY-MM-DD) |
computedAt |
long | Epoch seconds when this rollup was computed |
coverage |
object | Coverage block (see below) |
weightedConsensus |
object | Weighted consensus (see below) |
topContributors |
array | Top 10 contributors by absolute contribution to the weighted upside |
Coverage object (same shape on all three aggregate endpoints):
| Field | Type | Description |
|---|---|---|
holdingsCount |
int | Holdings present in holdings.json |
holdingsCovered |
int | Holdings that had per-stock data and were included in the aggregate |
weightCovered |
decimal | Sum of weights (0–100) for the covered holdings |
partial |
boolean | null | true when the underlying composition is a top-N view |
totalKnownHoldings |
int | null | Issuer's true holdings count when partial=true |
Weighted consensus object:
| Field | Type | Description |
|---|---|---|
upsidePercent |
decimal | Weighted 12-month upside as a percentage (e.g. 12.4 = +12.4%) |
consensusLabel |
string | Most-weighted bucket: BUY / HOLD / SELL |
distribution |
object | Fractions of covered AUM in each bucket: { "BUY": 0.62, "HOLD": 0.31, "SELL": 0.07 } |
totalAnalysts |
int | Sum of analyst counts across covered holdings |
Contributor object:
| Field | Type | Description |
|---|---|---|
ticker |
string | Constituent ticker |
weightPct |
decimal | Holding weight in the fund (0–100) |
upsidePercent |
decimal | Per-stock analyst upside |
consensusLabel |
string | Per-stock consensus bucket |
contributionPp |
decimal | Signed contribution to the fund's weighted upside, in percentage points |
Returns 404 with {error, reason, message} when the ticker isn't an ETF or when coverage is too low to publish a defensible aggregate (typically foreign-listed funds where most holdings lack per-stock data).
GET /{ticker}/aggregates/insider
Returns a holdings-weighted insider activity aggregate for an ETF: net dollar flow, buy/sell split, and per-holding contributors over a configurable trailing window.
Authentication: API key required. Returns the full response (including topContributors) to every API caller; PRO and FREE differ only in per-tier rate limits and monthly quota.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
ticker |
path | Yes | - | ETF ticker |
lookbackDays |
int | No | 30 | Trailing days of insider trades to include (typical values: 30, 90) |
Example Request:
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/ARKK/aggregates/insider?lookbackDays=90"
result = client.get_etf_insider_aggregate("ARKK", lookback_days=90)
flow = result["data"]["weightedNetFlow"]
print(f"Net insider flow: ${flow['netDollars']:,} ({flow['buyTradeCount']} buys, {flow['sellTradeCount']} sells)")
Aggregate object:
| Field | Type | Description |
|---|---|---|
ticker |
string | ETF ticker |
asOfDate |
string | Composition snapshot date (ISO date YYYY-MM-DD) |
computedAt |
long | Epoch seconds when this rollup was computed |
lookbackDays |
int | Window included in the trade aggregation |
coverage |
object | Coverage block (same shape as analyst aggregate) |
weightedNetFlow |
object | Aggregated flow (see below) |
topContributors |
array | Top 10 contributors by absolute weighted-net-dollar contribution |
Weighted net flow object:
| Field | Type | Description |
|---|---|---|
netDollars |
long | Weighted net dollar flow (buys − sells). Negative = net selling. |
buyDollars |
long | Weighted gross buy dollars |
sellDollars |
long | Weighted gross sell dollars |
buyTradeCount |
long | Total buy trades across covered holdings (unweighted, for context) |
sellTradeCount |
long | Total sell trades across covered holdings (unweighted, for context) |
distinctInsiderCount |
int | Distinct insiders across covered holdings |
Contributor object:
| Field | Type | Description |
|---|---|---|
ticker |
string | Constituent ticker |
weightPct |
decimal | Holding weight in the fund (0–100) |
netDollars |
long | Per-stock net insider flow over the window (signed) |
weightedNetDollars |
long | Signed contribution to the fund's weighted net flow |
tradeCount |
int | Per-stock trade count over the window |
Returns 404 when no holdings have insider activity over the window or coverage is insufficient.
GET /{ticker}/aggregates/sentiment
Beta as of 2026-05-15. The constituent-weighted score is precomputed daily for a growing set of widely-traded funds (SPY, QQQ, VOO, VTI, IWM, major SPDR sector funds, and more) and is expanding as we ingest more ETF data. Returns 404 for funds outside the current coverage window.
Returns two complementary SentiSense Score readings for an ETF side-by-side:
- Constituent-Weighted — daily-precomputed weighted average across the fund's constituents (the new view).
- Direct — score derived from mentions of the fund's own ticker (the existing per-ETF
sentisense_score).
These can diverge meaningfully — bull market chatter about a fund itself may not match the sentiment around its individual holdings, and the gap can be a signal.
Authentication: API key required. Response uses the standard {isPreview, previewReason, data} wrapper; the headline is the same for FREE and PRO (no per-holding contributors are included in v1).
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
ticker |
path | Yes | - | ETF ticker |
Example Request:
curl -H "X-SentiSense-API-Key: ss_live_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/QQQ/aggregates/sentiment"
result = client.get_etf_sentiment_aggregate("QQQ")
agg = result["data"]
print(f"Constituent-weighted: {agg['constituentsWeighted']['sentiSenseScore']} ({agg['constituentsWeighted']['scoreLabel']})")
if agg.get("direct"):
print(f"Direct (mentions of QQQ itself): {agg['direct']['sentiSenseScore']} ({agg['direct']['scoreLabel']})")
Aggregate object:
| Field | Type | Description |
|---|---|---|
ticker |
string | ETF ticker |
asOfDate |
string | null | Composition snapshot date the weighted view was computed against (ISO date YYYY-MM-DD) |
computedAt |
long | Epoch seconds when this aggregate was assembled |
coverage |
object | Coverage block (same shape as analyst aggregate) |
constituentsWeighted |
object | Weighted reading (see below) |
direct |
object | null | Direct reading; null for low-mention funds with no daily score |
Reading object (used for both constituentsWeighted and direct):
| Field | Type | Description |
|---|---|---|
sentiSenseScore |
decimal | SentiSense Score reading (signed; magnitude reflects volume × sentiment) |
scoreLabel |
string | Coarse-grained label: BULLISH / NEUTRAL / BEARISH |
asOfTimestamp |
long | Epoch seconds when the underlying metric was produced |
Returns 404 when the ticker isn't an ETF or the constituent-weighted metric hasn't been produced yet (brand-new ETFs may show 404 until the next daily pipeline run).
Errors
| Status | Code | Description |
|---|---|---|
| 400 | invalid_parameter | Invalid ticker or out-of-range lookbackDays |
| 401 | api_key_required | No API key on a programmatic call to an aggregate endpoint |
| 403 | quota_exceeded | Monthly request quota for your tier is used up |
| 404 | not_found | ETF not tracked, no composition data, or insufficient coverage for an aggregate |
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/etfs/
List all ETFs tracked by SentiSense
curl -H "X-SentiSense-API-Key: ssk_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/"GET/api/v1/etfs/{ticker}/holdings
Composition for an ETF (constituents + weights + freshness)
curl -H "X-SentiSense-API-Key: ssk_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/AAPL/holdings"GET/api/v1/etfs/{ticker}/quote
ETF detail-page quote: live price + AUM + expense ratio + NAV + inception
curl -H "X-SentiSense-API-Key: ssk_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/AAPL/quote"GET/api/v1/etfs/{ticker}/aggregates/analyst
Holdings-weighted analyst consensus + upside across constituents
curl -H "X-SentiSense-API-Key: ssk_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/AAPL/aggregates/analyst"GET/api/v1/etfs/{ticker}/aggregates/insider
Holdings-weighted insider net flow across constituents
curl -H "X-SentiSense-API-Key: ssk_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/AAPL/aggregates/insider"GET/api/v1/etfs/{ticker}/aggregates/sentiment
Constituent-weighted SentiSense Score + direct fund-level score side-by-side
curl -H "X-SentiSense-API-Key: ssk_YOUR_KEY" \
"https://app.sentisense.ai/api/v1/etfs/AAPL/aggregates/sentiment"