Mastering the Analyst Insights API: Using Real-Time Analyst Research to Track Market Sentiment Shifts

Mastering the Analyst Insights API: Using Real-Time Analyst Research to Track Market Sentiment Shifts

Mastering the Analyst Insights API: Using Real-Time Analyst Research to Track Market Sentiment Shifts

Learn how to track market sentiment shifts using the Analyst Insights API. Master PT drift, watchlist ranking, and sector sentiment scores in this developer-focused guide.

by Thomas Cotter

January 14, 2026


Markets don’t react to analyst ratings alone. They react to how analyst thinking evolves in real time. Before a formal upgrade, downgrade, or price target change shows up on a calendar, there is usually a trail of commentary, conviction shifts, and subtle narrative changes that influence institutional positioning.

That layer is easy to miss if you only look at ratings or headlines. The Analyst Insights API is designed to fill that gap. It captures analyst commentary as it is published, giving developers access to real-time research signals that sit between news and formal ratings.

In this guide, we focus on how this endpoint can be used as part of a modern stock API workflow. I’ll walk through what the data represents, how it differs from ratings, and how to extract meaningful signals from analyst research rather than treating it as static text. The goal is not to summarize opinions, but to understand how analyst sentiment forms and changes before the market fully prices it in.

How Analyst Research Actually Moves Markets

Analyst research rarely arrives as a single, decisive event. In practice, it unfolds gradually. An analyst might start by softening language around growth, flagging risks in margins, or emphasizing execution concerns. None of this triggers an immediate rating change, but it does influence how institutions frame the stock.

How commentary changes before ratings

Before a formal upgrade or downgrade, there is usually a sequence of smaller signals. Analysts adjust their tone, highlight new risks, or reframe expectations around upcoming catalysts. These shifts are subtle, but they matter because they often precede changes in positioning by larger market participants.

Where analyst insights sit in the market timeline

Analyst insights tend to sit between news and ratings in terms of speed. They react faster than scheduled rating changes but with more context than headlines. This makes them especially useful for understanding why a stock continues to move days after a news event or why momentum fades despite no visible downgrade.

Using analyst data effectively requires focusing on this progression, not just the endpoint. The Analyst Insights API is built to surface these evolving views, allowing you to track sentiment changes as they happen rather than after they are formalized.

Analyst Insights API Walkthrough

This section focuses on how the Analyst Insights API works in practice. We’ll look at the base request, supported query parameters, and the structure of the response you actually receive.

Base Request

A minimal request to the Analyst Insights endpoint looks like this:


import requests
 
api_key = "YOUR BENZINGA API KEY"
url = "https://api.benzinga.com/api/v1/analyst/insights"
 
headers = {"accept": "application/json"}
params = {
    "token": api_key,
    "symbols": "AAPL",
    "pageSize": 5
}
 
r = requests.get(url, params=params, headers=headers)
data = r.json()['analyst-insights']
data[0]
import requests
 
api_key = "YOUR BENZINGA API KEY"
url = "https://api.benzinga.com/api/v1/analyst/insights"
 
headers = {"accept": "application/json"}
params = {
    "token": api_key,
    "symbols": "AAPL",
    "pageSize": 5
}
 
r = requests.get(url, params=params, headers=headers)
data = r.json()['analyst-insights']
data[0]
import requests
 
api_key = "YOUR BENZINGA API KEY"
url = "https://api.benzinga.com/api/v1/analyst/insights"
 
headers = {"accept": "application/json"}
params = {
    "token": api_key,
    "symbols": "AAPL",
    "pageSize": 5
}
 
r = requests.get(url, params=params, headers=headers)
data = r.json()['analyst-insights']
data[0]
import requests
 
api_key = "YOUR BENZINGA API KEY"
url = "https://api.benzinga.com/api/v1/analyst/insights"
 
headers = {"accept": "application/json"}
params = {
    "token": api_key,
    "symbols": "AAPL",
    "pageSize": 5
}
 
r = requests.get(url, params=params, headers=headers)
data = r.json()['analyst-insights']
data[0]

The response is a list of analyst insight objects, each representing a single published research note tied to a specific stock.


{'action': 'Reiterates',
 'analyst_id': '692ddb8208782e0001f06b35',
 'analyst_insights': "Wedbush reiterated their Outperform rating on Apple's stock with a price target of $350.00.  \n\n **Robust Product Strategy in China**: Apple's continued success and strategic positioning in the Chinese market, a critical area for growth in the coming years, has likely contributed to the confidence in the company's future performance. Despite the challenges posed by competition and macroeconomic factors such as tariffs, Apple's ability to sustain its market presence and navigate through these risks reinforces its potential for long-term growth.\n\n**Stepping Stone for AI Strategy Acceleration**: Apple's incremental progress in its artificial intelligence (AI) strategy, particularly with the integration expected to enhance Siri and potentially introduce new subscription-based revenue streams, signals a significant advancement for the company. This move, validated by Google's foundational model and cleared by legal hurdles, positions Apple to capitalize on the consumer AI revolution, further bolstering the optimistic outlook on its stock.",
 'date': '2026-01-13',
 'firm': 'Wedbush',
 'firm_id': '606ae6206538960001bcea8a',
 'id': '69664ecfc4edb597fd6b7f15',
 'pt': '350.00',
 'rating': 'Outperform',
 'rating_id': '69664e86ecc0340001c3bab5',
 'security': {'cik': '320193',
  'exchange': 'NASDAQ',
  'isin': 'US0378331005',
  'name': 'Apple',
  'symbol': 'AAPL'},
 'updated': 1768312538}
{'action': 'Reiterates',
 'analyst_id': '692ddb8208782e0001f06b35',
 'analyst_insights': "Wedbush reiterated their Outperform rating on Apple's stock with a price target of $350.00.  \n\n **Robust Product Strategy in China**: Apple's continued success and strategic positioning in the Chinese market, a critical area for growth in the coming years, has likely contributed to the confidence in the company's future performance. Despite the challenges posed by competition and macroeconomic factors such as tariffs, Apple's ability to sustain its market presence and navigate through these risks reinforces its potential for long-term growth.\n\n**Stepping Stone for AI Strategy Acceleration**: Apple's incremental progress in its artificial intelligence (AI) strategy, particularly with the integration expected to enhance Siri and potentially introduce new subscription-based revenue streams, signals a significant advancement for the company. This move, validated by Google's foundational model and cleared by legal hurdles, positions Apple to capitalize on the consumer AI revolution, further bolstering the optimistic outlook on its stock.",
 'date': '2026-01-13',
 'firm': 'Wedbush',
 'firm_id': '606ae6206538960001bcea8a',
 'id': '69664ecfc4edb597fd6b7f15',
 'pt': '350.00',
 'rating': 'Outperform',
 'rating_id': '69664e86ecc0340001c3bab5',
 'security': {'cik': '320193',
  'exchange': 'NASDAQ',
  'isin': 'US0378331005',
  'name': 'Apple',
  'symbol': 'AAPL'},
 'updated': 1768312538}
{'action': 'Reiterates',
 'analyst_id': '692ddb8208782e0001f06b35',
 'analyst_insights': "Wedbush reiterated their Outperform rating on Apple's stock with a price target of $350.00.  \n\n **Robust Product Strategy in China**: Apple's continued success and strategic positioning in the Chinese market, a critical area for growth in the coming years, has likely contributed to the confidence in the company's future performance. Despite the challenges posed by competition and macroeconomic factors such as tariffs, Apple's ability to sustain its market presence and navigate through these risks reinforces its potential for long-term growth.\n\n**Stepping Stone for AI Strategy Acceleration**: Apple's incremental progress in its artificial intelligence (AI) strategy, particularly with the integration expected to enhance Siri and potentially introduce new subscription-based revenue streams, signals a significant advancement for the company. This move, validated by Google's foundational model and cleared by legal hurdles, positions Apple to capitalize on the consumer AI revolution, further bolstering the optimistic outlook on its stock.",
 'date': '2026-01-13',
 'firm': 'Wedbush',
 'firm_id': '606ae6206538960001bcea8a',
 'id': '69664ecfc4edb597fd6b7f15',
 'pt': '350.00',
 'rating': 'Outperform',
 'rating_id': '69664e86ecc0340001c3bab5',
 'security': {'cik': '320193',
  'exchange': 'NASDAQ',
  'isin': 'US0378331005',
  'name': 'Apple',
  'symbol': 'AAPL'},
 'updated': 1768312538}
{'action': 'Reiterates',
 'analyst_id': '692ddb8208782e0001f06b35',
 'analyst_insights': "Wedbush reiterated their Outperform rating on Apple's stock with a price target of $350.00.  \n\n **Robust Product Strategy in China**: Apple's continued success and strategic positioning in the Chinese market, a critical area for growth in the coming years, has likely contributed to the confidence in the company's future performance. Despite the challenges posed by competition and macroeconomic factors such as tariffs, Apple's ability to sustain its market presence and navigate through these risks reinforces its potential for long-term growth.\n\n**Stepping Stone for AI Strategy Acceleration**: Apple's incremental progress in its artificial intelligence (AI) strategy, particularly with the integration expected to enhance Siri and potentially introduce new subscription-based revenue streams, signals a significant advancement for the company. This move, validated by Google's foundational model and cleared by legal hurdles, positions Apple to capitalize on the consumer AI revolution, further bolstering the optimistic outlook on its stock.",
 'date': '2026-01-13',
 'firm': 'Wedbush',
 'firm_id': '606ae6206538960001bcea8a',
 'id': '69664ecfc4edb597fd6b7f15',
 'pt': '350.00',
 'rating': 'Outperform',
 'rating_id': '69664e86ecc0340001c3bab5',
 'security': {'cik': '320193',
  'exchange': 'NASDAQ',
  'isin': 'US0378331005',
  'name': 'Apple',
  'symbol': 'AAPL'},
 'updated': 1768312538}

Query Parameters

Most use cases rely on a small subset of parameters.

Parameter

Description

symbols

One or more stock symbols, comma-separated.

analyst

Filter by analyst ID(s).

rating_id

Filter by rating ID(s).

search_keys_type

Used when filtering by firm or firm ID.

search_keys

Firm name or firm ID, depending on search_keys_type.

page

Page number for pagination.

pageSize

Number of records per page.

These parameters make it possible to query insights by stock, analyst, or research firm, depending on how narrowly you want to scope the data.

Response Structure

Each object in the response contains both the analyst commentary and the metadata needed to work with it programmatically.

Core Insight Fields:

Field

Meaning

id

Unique identifier for the insight.

date

Date the insight was published.

updated

Last update timestamp.

analyst_insights

The analyst’s written commentary.

action

Associated action, such as Maintains or Reiterates.

rating

Current rating at the time of the insight.

pt

Price target, if provided.


Firm and Security Metadata:

Field

Meaning

firm

Analyst firm name.

firm_id

Firm identifier.

rating_id

Identifier for the rating.

security.symbol

Stock ticker.

security.name

Company name.

security.exchange

Listing exchange.

security.isin

ISIN, when available.

The key thing to note is that the insight text itself is structured enough to be filtered and grouped, but still flexible enough to capture qualitative commentary. This is what makes the endpoint different from ratings or calendar-based APIs.

Use Case 1: Tracking Analyst Sentiment Drift Over Time

Ratings change infrequently. Analyst commentary does not. This makes analyst insights useful for tracking how sentiment evolves between formal actions. In this use case, the goal is to observe whether analyst views are becoming more constructive or more cautious over time, even when the rating stays the same.

Pulling Analyst Insights for a Single Stock

We start by fetching all available insights for a single ticker and sorting them chronologically.


import pandas as pd
import requests
 
api_key = "YOUR BENZINGA API KEY"
url = "https://api.benzinga.com/api/v1/analyst/insights"
 
headers = {"accept": "application/json"}
params = {
    "token": api_key,
    "symbols": "AAPL",
    "pageSize": 50
}
 
r = requests.get(url, params=params, headers=headers)
insights = r.json()
 
df = pd.DataFrame(insights)
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date")
 
df[["date", "firm", "action", "rating"]].head()
import pandas as pd
import requests
 
api_key = "YOUR BENZINGA API KEY"
url = "https://api.benzinga.com/api/v1/analyst/insights"
 
headers = {"accept": "application/json"}
params = {
    "token": api_key,
    "symbols": "AAPL",
    "pageSize": 50
}
 
r = requests.get(url, params=params, headers=headers)
insights = r.json()
 
df = pd.DataFrame(insights)
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date")
 
df[["date", "firm", "action", "rating"]].head()
import pandas as pd
import requests
 
api_key = "YOUR BENZINGA API KEY"
url = "https://api.benzinga.com/api/v1/analyst/insights"
 
headers = {"accept": "application/json"}
params = {
    "token": api_key,
    "symbols": "AAPL",
    "pageSize": 50
}
 
r = requests.get(url, params=params, headers=headers)
insights = r.json()
 
df = pd.DataFrame(insights)
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date")
 
df[["date", "firm", "action", "rating"]].head()
import pandas as pd
import requests
 
api_key = "YOUR BENZINGA API KEY"
url = "https://api.benzinga.com/api/v1/analyst/insights"
 
headers = {"accept": "application/json"}
params = {
    "token": api_key,
    "symbols": "AAPL",
    "pageSize": 50
}
 
r = requests.get(url, params=params, headers=headers)
insights = r.json()
 
df = pd.DataFrame(insights)
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date")
 
df[["date", "firm", "action", "rating"]].head()


At this stage, we are not interpreting the commentary. We are simply establishing the sequence of analyst updates tied to the stock.

Looking for Repetition and Changes in Framing

One of the simplest ways to detect sentiment drift is to look at how often analysts revisit the same themes. This can be done by grouping insights by firm and examining how frequently commentary appears without a rating change.


summary = (
    df.groupby(["firm", "rating"])
      .size()
      .reset_index(name="insight_count")
      .sort_values("insight_count", ascending=False)
)
 
summary.head()
summary = (
    df.groupby(["firm", "rating"])
      .size()
      .reset_index(name="insight_count")
      .sort_values("insight_count", ascending=False)
)
 
summary.head()
summary = (
    df.groupby(["firm", "rating"])
      .size()
      .reset_index(name="insight_count")
      .sort_values("insight_count", ascending=False)
)
 
summary.head()
summary = (
    df.groupby(["firm", "rating"])
      .size()
      .reset_index(name="insight_count")
      .sort_values("insight_count", ascending=False)
)
 
summary.head()


When analysts publish multiple insights under the same rating, it usually signals ongoing reassessment rather than a static view. This often precedes either a stronger conviction or a reversal.

Why This Matters in Practice

This type of analysis is not about labeling sentiment as bullish or bearish. It is about understanding momentum in analyst thinking. A stock with repeated cautious commentary under a maintained rating can behave very differently from one with sparse updates and stable language.

Used alongside prices or ratings data, this approach helps avoid treating analyst opinion as binary. It introduces time and persistence into how analyst research is interpreted, which is where much of its real value lies.

Use Case 2: Turning a Stock List Into a Research-Ready Watchlist With Analyst Signals

A raw stock list is easy to get. What’s hard is deciding which names deserve attention today.

This is where the Analyst Insights API fits nicely in a stock API workflow. Instead of treating analyst research as static text, we can turn it into a lightweight “research layer” on top of a watchlist. The output is a ranked table where each ticker has a few simple signals that explain why it is moving up or down the list.

What we’ll build:

Given a list of tickers, we’ll pull recent analyst insights for each one and compute a small set of interpretable signals:

  • Latest rating and price target

  • Price target drift over recent insights

  • Insight velocity (how frequently analysts are publishing)

  • Firm diversity (how many distinct firms are contributing)

  • Action mix (whether we’re seeing upgrades or just reiterations)

At the end, we’ll combine these into a simple score that helps you prioritize research, not blindly “buy” or “sell”.

Step 1: Fresh data extraction for a watchlist

We start with a small watchlist. In a real product, this could come from a screener, portfolio holdings, or user watchlists.


watchlist = ["AAPL", "MSFT", "NVDA", "AMZN", "TSLA"]
 
def fetch_insights(symbol, page_size=75):
    params = {
        "token": api_key,
        "symbols": symbol,
        "pageSize": page_size
    }
    r = requests.get(url, params=params, headers=headers)
    j = r.json()
    items = j.get("analyst-insights", j)
    if not isinstance(items, list):
        items = []
    df = pd.DataFrame(items)
    if df.empty:
        return df
    df["symbol"] = symbol
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["pt"] = pd.to_numeric(df.get("pt"), errors="coerce")
    return df
 
frames = []
for sym in watchlist:
    frames.append(fetch_insights(sym))
 
insights_df = pd.concat(frames, ignore_index=True)
insights_df = insights_df.dropna(subset=["date"])
insights_df = insights_df.sort_values(["symbol", "date"])
insights_df.head()
watchlist = ["AAPL", "MSFT", "NVDA", "AMZN", "TSLA"]
 
def fetch_insights(symbol, page_size=75):
    params = {
        "token": api_key,
        "symbols": symbol,
        "pageSize": page_size
    }
    r = requests.get(url, params=params, headers=headers)
    j = r.json()
    items = j.get("analyst-insights", j)
    if not isinstance(items, list):
        items = []
    df = pd.DataFrame(items)
    if df.empty:
        return df
    df["symbol"] = symbol
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["pt"] = pd.to_numeric(df.get("pt"), errors="coerce")
    return df
 
frames = []
for sym in watchlist:
    frames.append(fetch_insights(sym))
 
insights_df = pd.concat(frames, ignore_index=True)
insights_df = insights_df.dropna(subset=["date"])
insights_df = insights_df.sort_values(["symbol", "date"])
insights_df.head()
watchlist = ["AAPL", "MSFT", "NVDA", "AMZN", "TSLA"]
 
def fetch_insights(symbol, page_size=75):
    params = {
        "token": api_key,
        "symbols": symbol,
        "pageSize": page_size
    }
    r = requests.get(url, params=params, headers=headers)
    j = r.json()
    items = j.get("analyst-insights", j)
    if not isinstance(items, list):
        items = []
    df = pd.DataFrame(items)
    if df.empty:
        return df
    df["symbol"] = symbol
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["pt"] = pd.to_numeric(df.get("pt"), errors="coerce")
    return df
 
frames = []
for sym in watchlist:
    frames.append(fetch_insights(sym))
 
insights_df = pd.concat(frames, ignore_index=True)
insights_df = insights_df.dropna(subset=["date"])
insights_df = insights_df.sort_values(["symbol", "date"])
insights_df.head()
watchlist = ["AAPL", "MSFT", "NVDA", "AMZN", "TSLA"]
 
def fetch_insights(symbol, page_size=75):
    params = {
        "token": api_key,
        "symbols": symbol,
        "pageSize": page_size
    }
    r = requests.get(url, params=params, headers=headers)
    j = r.json()
    items = j.get("analyst-insights", j)
    if not isinstance(items, list):
        items = []
    df = pd.DataFrame(items)
    if df.empty:
        return df
    df["symbol"] = symbol
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["pt"] = pd.to_numeric(df.get("pt"), errors="coerce")
    return df
 
frames = []
for sym in watchlist:
    frames.append(fetch_insights(sym))
 
insights_df = pd.concat(frames, ignore_index=True)
insights_df = insights_df.dropna(subset=["date"])
insights_df = insights_df.sort_values(["symbol", "date"])
insights_df.head()

This gives us a single dataset across multiple tickers, with each insight tied to a symbol, firm, rating, action, and possibly a price target.


Step 2: Compute signal features per ticker

Now we extract a few features that are both simple and useful.


def build_signals(df):
    df = df.sort_values("date")
    latest = df.iloc[-1]
 
    latest_rating = latest.get("rating")
    latest_pt = latest.get("pt")
 
    df["pt_prev"] = df["pt"].shift(1)
    df["pt_change_pct"] = (df["pt"] - df["pt_prev"]) / df["pt_prev"]
 
    pt_drift = df["pt_change_pct"].dropna()
    pt_drift_recent = pt_drift.tail(5)
 
    pt_drift_score = pt_drift_recent.sum() if len(pt_drift_recent) > 0 else 0.0
 
    recent_60d = df[df["date"] >= (df["date"].max() - pd.Timedelta(days=60))]
    insight_velocity = len(recent_60d)
 
    firm_diversity = recent_60d["firm"].nunique() if "firm" in recent_60d.columns else 0
 
    major_actions = recent_60d["action"].isin(["Upgrades", "Downgrades"]).sum() if "action" in recent_60d.columns else 0
 
    return pd.Series({
        "latest_rating": latest_rating,
        "latest_pt": latest_pt,
        "pt_drift_recent_sum": pt_drift_score,
        "insights_last_60d": insight_velocity,
        "unique_firms_last_60d": firm_diversity,
        "major_actions_last_60d": major_actions
    })
 
signal_table = (
    insights_df.groupby("symbol", group_keys=False)
              .apply(build_signals)
              .reset_index()
)
 
signal_table
def build_signals(df):
    df = df.sort_values("date")
    latest = df.iloc[-1]
 
    latest_rating = latest.get("rating")
    latest_pt = latest.get("pt")
 
    df["pt_prev"] = df["pt"].shift(1)
    df["pt_change_pct"] = (df["pt"] - df["pt_prev"]) / df["pt_prev"]
 
    pt_drift = df["pt_change_pct"].dropna()
    pt_drift_recent = pt_drift.tail(5)
 
    pt_drift_score = pt_drift_recent.sum() if len(pt_drift_recent) > 0 else 0.0
 
    recent_60d = df[df["date"] >= (df["date"].max() - pd.Timedelta(days=60))]
    insight_velocity = len(recent_60d)
 
    firm_diversity = recent_60d["firm"].nunique() if "firm" in recent_60d.columns else 0
 
    major_actions = recent_60d["action"].isin(["Upgrades", "Downgrades"]).sum() if "action" in recent_60d.columns else 0
 
    return pd.Series({
        "latest_rating": latest_rating,
        "latest_pt": latest_pt,
        "pt_drift_recent_sum": pt_drift_score,
        "insights_last_60d": insight_velocity,
        "unique_firms_last_60d": firm_diversity,
        "major_actions_last_60d": major_actions
    })
 
signal_table = (
    insights_df.groupby("symbol", group_keys=False)
              .apply(build_signals)
              .reset_index()
)
 
signal_table
def build_signals(df):
    df = df.sort_values("date")
    latest = df.iloc[-1]
 
    latest_rating = latest.get("rating")
    latest_pt = latest.get("pt")
 
    df["pt_prev"] = df["pt"].shift(1)
    df["pt_change_pct"] = (df["pt"] - df["pt_prev"]) / df["pt_prev"]
 
    pt_drift = df["pt_change_pct"].dropna()
    pt_drift_recent = pt_drift.tail(5)
 
    pt_drift_score = pt_drift_recent.sum() if len(pt_drift_recent) > 0 else 0.0
 
    recent_60d = df[df["date"] >= (df["date"].max() - pd.Timedelta(days=60))]
    insight_velocity = len(recent_60d)
 
    firm_diversity = recent_60d["firm"].nunique() if "firm" in recent_60d.columns else 0
 
    major_actions = recent_60d["action"].isin(["Upgrades", "Downgrades"]).sum() if "action" in recent_60d.columns else 0
 
    return pd.Series({
        "latest_rating": latest_rating,
        "latest_pt": latest_pt,
        "pt_drift_recent_sum": pt_drift_score,
        "insights_last_60d": insight_velocity,
        "unique_firms_last_60d": firm_diversity,
        "major_actions_last_60d": major_actions
    })
 
signal_table = (
    insights_df.groupby("symbol", group_keys=False)
              .apply(build_signals)
              .reset_index()
)
 
signal_table
def build_signals(df):
    df = df.sort_values("date")
    latest = df.iloc[-1]
 
    latest_rating = latest.get("rating")
    latest_pt = latest.get("pt")
 
    df["pt_prev"] = df["pt"].shift(1)
    df["pt_change_pct"] = (df["pt"] - df["pt_prev"]) / df["pt_prev"]
 
    pt_drift = df["pt_change_pct"].dropna()
    pt_drift_recent = pt_drift.tail(5)
 
    pt_drift_score = pt_drift_recent.sum() if len(pt_drift_recent) > 0 else 0.0
 
    recent_60d = df[df["date"] >= (df["date"].max() - pd.Timedelta(days=60))]
    insight_velocity = len(recent_60d)
 
    firm_diversity = recent_60d["firm"].nunique() if "firm" in recent_60d.columns else 0
 
    major_actions = recent_60d["action"].isin(["Upgrades", "Downgrades"]).sum() if "action" in recent_60d.columns else 0
 
    return pd.Series({
        "latest_rating": latest_rating,
        "latest_pt": latest_pt,
        "pt_drift_recent_sum": pt_drift_score,
        "insights_last_60d": insight_velocity,
        "unique_firms_last_60d": firm_diversity,
        "major_actions_last_60d": major_actions
    })
 
signal_table = (
    insights_df.groupby("symbol", group_keys=False)
              .apply(build_signals)
              .reset_index()
)
 
signal_table


This is already useful in a research tool. You can scan which stocks have rising targets, increasing analyst activity, or more firms paying attention.

Step 3: Score and rank the watchlist

Now we convert these signals into a simple ranking.

The goal is not to predict returns. It’s to push the most “research-active” names to the top.


scored = signal_table.copy()
 
scored["score"] = 0.0
 
scored["score"] += (scored["pt_drift_recent_sum"].clip(-0.2, 0.2) * 10)
scored["score"] += (scored["unique_firms_last_60d"].clip(0, 10) * 0.5)
scored["score"] += (scored["insights_last_60d"].clip(0, 30) * 0.1)
scored["score"] += (scored["major_actions_last_60d"].clip(0, 5) * 1.0)
 
ranked = scored.sort_values("score", ascending=False)
 
ranked
scored = signal_table.copy()
 
scored["score"] = 0.0
 
scored["score"] += (scored["pt_drift_recent_sum"].clip(-0.2, 0.2) * 10)
scored["score"] += (scored["unique_firms_last_60d"].clip(0, 10) * 0.5)
scored["score"] += (scored["insights_last_60d"].clip(0, 30) * 0.1)
scored["score"] += (scored["major_actions_last_60d"].clip(0, 5) * 1.0)
 
ranked = scored.sort_values("score", ascending=False)
 
ranked
scored = signal_table.copy()
 
scored["score"] = 0.0
 
scored["score"] += (scored["pt_drift_recent_sum"].clip(-0.2, 0.2) * 10)
scored["score"] += (scored["unique_firms_last_60d"].clip(0, 10) * 0.5)
scored["score"] += (scored["insights_last_60d"].clip(0, 30) * 0.1)
scored["score"] += (scored["major_actions_last_60d"].clip(0, 5) * 1.0)
 
ranked = scored.sort_values("score", ascending=False)
 
ranked
scored = signal_table.copy()
 
scored["score"] = 0.0
 
scored["score"] += (scored["pt_drift_recent_sum"].clip(-0.2, 0.2) * 10)
scored["score"] += (scored["unique_firms_last_60d"].clip(0, 10) * 0.5)
scored["score"] += (scored["insights_last_60d"].clip(0, 30) * 0.1)
scored["score"] += (scored["major_actions_last_60d"].clip(0, 5) * 1.0)
 
ranked = scored.sort_values("score", ascending=False)
 
ranked


In this output, the ranking is driven almost entirely by price target drift and coverage breadth, since major_actions_last_60d is 0 across all tickers. That means no upgrades or downgrades showed up in the last 60 days for this sample, so the main differentiators are whether targets are moving up or down and how many distinct firms are publishing.

NVDA ranks highest because it combines the strongest positive target drift (about 0.48) with the most active and broad research flow (10 insights from 10 firms). MSFT ranks second with low volume but a strong positive drift (about 0.44), suggesting fewer but more meaningful updates. AMZN and AAPL sit in the middle with modest positive drift, with AMZN edging ahead due to broader firm coverage, while TSLA lands last because the recent target drift is sharply negative (about -0.94), which overwhelms its otherwise decent activity levels.

Use Case 3: Sector-Level Analyst Sentiment Score Using Analyst Insights

A single stock view is useful, but Analyst Insights becomes far more interesting when you zoom out. If you’re building stock APIs or research tooling, you usually want to answer broader questions like:

Is analyst conviction rising in a sector? Or is it cooling off?

In this use case, we’ll take a sector, pull a basket of tickers in that sector, fetch analyst insights for each ticker, score them, and then compute an average sector score that tells us whether the sector is seeing improving or weakening analyst sentiment.

Step 1: Pick a sector and its ticker set

For this example, we’ll assume we’re working with a tech basket. In a real workflow, you would pull the constituents dynamically from a sector classification dataset, but for an endpoint guide, a static list is enough to demonstrate the concept.


sector_name = "Technology"
sector_tickers = ["AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "AMD", "TSM", "ORCL", "CRM"]
sector_name = "Technology"
sector_tickers = ["AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "AMD", "TSM", "ORCL", "CRM"]
sector_name = "Technology"
sector_tickers = ["AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "AMD", "TSM", "ORCL", "CRM"]
sector_name = "Technology"
sector_tickers = ["AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "AMD", "TSM", "ORCL", "CRM"]

Step 2: Fresh data extraction across the sector

We’ll pull a larger dataset for each ticker, and we’ll keep only a recent time window so the score reflects current sentiment rather than old research.


def fetch_sector_insights(symbol, page_size=100):
    params = {
        "token": api_key,
        "symbols": symbol,
        "pageSize": page_size
    }
    r = requests.get(url, params=params, headers=headers)
    j = r.json()
    items = j.get("analyst-insights", j)
    if not isinstance(items, list):
        return pd.DataFrame()
    df = pd.DataFrame(items)
    if df.empty:
        return df
    df["symbol"] = symbol
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["pt"] = pd.to_numeric(df.get("pt"), errors="coerce")
    return df
 
sector_frames = [fetch_sector_insights(sym) for sym in sector_tickers]
sector_df = pd.concat(sector_frames, ignore_index=True).dropna(subset=["date"])
 
cutoff = sector_df["date"].max() - pd.Timedelta(days=90)
sector_df = sector_df[sector_df["date"] >= cutoff].sort_values(["symbol", "date"])
sector_df[["symbol", "date", "firm", "action", "rating", "pt"]].head()
def fetch_sector_insights(symbol, page_size=100):
    params = {
        "token": api_key,
        "symbols": symbol,
        "pageSize": page_size
    }
    r = requests.get(url, params=params, headers=headers)
    j = r.json()
    items = j.get("analyst-insights", j)
    if not isinstance(items, list):
        return pd.DataFrame()
    df = pd.DataFrame(items)
    if df.empty:
        return df
    df["symbol"] = symbol
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["pt"] = pd.to_numeric(df.get("pt"), errors="coerce")
    return df
 
sector_frames = [fetch_sector_insights(sym) for sym in sector_tickers]
sector_df = pd.concat(sector_frames, ignore_index=True).dropna(subset=["date"])
 
cutoff = sector_df["date"].max() - pd.Timedelta(days=90)
sector_df = sector_df[sector_df["date"] >= cutoff].sort_values(["symbol", "date"])
sector_df[["symbol", "date", "firm", "action", "rating", "pt"]].head()
def fetch_sector_insights(symbol, page_size=100):
    params = {
        "token": api_key,
        "symbols": symbol,
        "pageSize": page_size
    }
    r = requests.get(url, params=params, headers=headers)
    j = r.json()
    items = j.get("analyst-insights", j)
    if not isinstance(items, list):
        return pd.DataFrame()
    df = pd.DataFrame(items)
    if df.empty:
        return df
    df["symbol"] = symbol
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["pt"] = pd.to_numeric(df.get("pt"), errors="coerce")
    return df
 
sector_frames = [fetch_sector_insights(sym) for sym in sector_tickers]
sector_df = pd.concat(sector_frames, ignore_index=True).dropna(subset=["date"])
 
cutoff = sector_df["date"].max() - pd.Timedelta(days=90)
sector_df = sector_df[sector_df["date"] >= cutoff].sort_values(["symbol", "date"])
sector_df[["symbol", "date", "firm", "action", "rating", "pt"]].head()
def fetch_sector_insights(symbol, page_size=100):
    params = {
        "token": api_key,
        "symbols": symbol,
        "pageSize": page_size
    }
    r = requests.get(url, params=params, headers=headers)
    j = r.json()
    items = j.get("analyst-insights", j)
    if not isinstance(items, list):
        return pd.DataFrame()
    df = pd.DataFrame(items)
    if df.empty:
        return df
    df["symbol"] = symbol
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["pt"] = pd.to_numeric(df.get("pt"), errors="coerce")
    return df
 
sector_frames = [fetch_sector_insights(sym) for sym in sector_tickers]
sector_df = pd.concat(sector_frames, ignore_index=True).dropna(subset=["date"])
 
cutoff = sector_df["date"].max() - pd.Timedelta(days=90)
sector_df = sector_df[sector_df["date"] >= cutoff].sort_values(["symbol", "date"])
sector_df[["symbol", "date", "firm", "action", "rating", "pt"]].head()


Step 3: Build per-stock sentiment signals

We’ll reuse the same signal logic as the watchlist, but now it’s being applied to all tickers inside a sector.


def compute_stock_score(df):
    df = df.sort_values("date")
    latest = df.iloc[-1]
 
    df["pt_prev"] = df["pt"].shift(1)
    df["pt_change_pct"] = (df["pt"] - df["pt_prev"]) / df["pt_prev"]
 
    pt_drift = df["pt_change_pct"].dropna().tail(5)
    pt_drift_sum = pt_drift.sum() if len(pt_drift) > 0 else 0.0
 
    insight_velocity = len(df)
    firm_diversity = df["firm"].nunique() if "firm" in df.columns else 0
    major_actions = df["action"].isin(["Upgrades", "Downgrades"]).sum() if "action" in df.columns else 0
 
    score = 0.0
    score += (max(min(pt_drift_sum, 0.2), -0.2) * 10)
    score += (min(firm_diversity, 10) * 0.5)
    score += (min(insight_velocity, 30) * 0.1)
    score += (min(major_actions, 5) * 1.0)
 
    return pd.Series({
        "latest_rating": latest.get("rating"),
        "latest_pt": latest.get("pt"),
        "pt_drift_recent_sum": pt_drift_sum,
        "insights_last_90d": insight_velocity,
        "unique_firms_last_90d": firm_diversity,
        "major_actions_last_90d": major_actions,
        "score": score
    })
 
stock_scores = (
    sector_df.groupby("symbol", group_keys=False)
             .apply(compute_stock_score)
             .reset_index()
             .sort_values("score", ascending=False)
)
 
stock_scores.head(10)
def compute_stock_score(df):
    df = df.sort_values("date")
    latest = df.iloc[-1]
 
    df["pt_prev"] = df["pt"].shift(1)
    df["pt_change_pct"] = (df["pt"] - df["pt_prev"]) / df["pt_prev"]
 
    pt_drift = df["pt_change_pct"].dropna().tail(5)
    pt_drift_sum = pt_drift.sum() if len(pt_drift) > 0 else 0.0
 
    insight_velocity = len(df)
    firm_diversity = df["firm"].nunique() if "firm" in df.columns else 0
    major_actions = df["action"].isin(["Upgrades", "Downgrades"]).sum() if "action" in df.columns else 0
 
    score = 0.0
    score += (max(min(pt_drift_sum, 0.2), -0.2) * 10)
    score += (min(firm_diversity, 10) * 0.5)
    score += (min(insight_velocity, 30) * 0.1)
    score += (min(major_actions, 5) * 1.0)
 
    return pd.Series({
        "latest_rating": latest.get("rating"),
        "latest_pt": latest.get("pt"),
        "pt_drift_recent_sum": pt_drift_sum,
        "insights_last_90d": insight_velocity,
        "unique_firms_last_90d": firm_diversity,
        "major_actions_last_90d": major_actions,
        "score": score
    })
 
stock_scores = (
    sector_df.groupby("symbol", group_keys=False)
             .apply(compute_stock_score)
             .reset_index()
             .sort_values("score", ascending=False)
)
 
stock_scores.head(10)
def compute_stock_score(df):
    df = df.sort_values("date")
    latest = df.iloc[-1]
 
    df["pt_prev"] = df["pt"].shift(1)
    df["pt_change_pct"] = (df["pt"] - df["pt_prev"]) / df["pt_prev"]
 
    pt_drift = df["pt_change_pct"].dropna().tail(5)
    pt_drift_sum = pt_drift.sum() if len(pt_drift) > 0 else 0.0
 
    insight_velocity = len(df)
    firm_diversity = df["firm"].nunique() if "firm" in df.columns else 0
    major_actions = df["action"].isin(["Upgrades", "Downgrades"]).sum() if "action" in df.columns else 0
 
    score = 0.0
    score += (max(min(pt_drift_sum, 0.2), -0.2) * 10)
    score += (min(firm_diversity, 10) * 0.5)
    score += (min(insight_velocity, 30) * 0.1)
    score += (min(major_actions, 5) * 1.0)
 
    return pd.Series({
        "latest_rating": latest.get("rating"),
        "latest_pt": latest.get("pt"),
        "pt_drift_recent_sum": pt_drift_sum,
        "insights_last_90d": insight_velocity,
        "unique_firms_last_90d": firm_diversity,
        "major_actions_last_90d": major_actions,
        "score": score
    })
 
stock_scores = (
    sector_df.groupby("symbol", group_keys=False)
             .apply(compute_stock_score)
             .reset_index()
             .sort_values("score", ascending=False)
)
 
stock_scores.head(10)
def compute_stock_score(df):
    df = df.sort_values("date")
    latest = df.iloc[-1]
 
    df["pt_prev"] = df["pt"].shift(1)
    df["pt_change_pct"] = (df["pt"] - df["pt_prev"]) / df["pt_prev"]
 
    pt_drift = df["pt_change_pct"].dropna().tail(5)
    pt_drift_sum = pt_drift.sum() if len(pt_drift) > 0 else 0.0
 
    insight_velocity = len(df)
    firm_diversity = df["firm"].nunique() if "firm" in df.columns else 0
    major_actions = df["action"].isin(["Upgrades", "Downgrades"]).sum() if "action" in df.columns else 0
 
    score = 0.0
    score += (max(min(pt_drift_sum, 0.2), -0.2) * 10)
    score += (min(firm_diversity, 10) * 0.5)
    score += (min(insight_velocity, 30) * 0.1)
    score += (min(major_actions, 5) * 1.0)
 
    return pd.Series({
        "latest_rating": latest.get("rating"),
        "latest_pt": latest.get("pt"),
        "pt_drift_recent_sum": pt_drift_sum,
        "insights_last_90d": insight_velocity,
        "unique_firms_last_90d": firm_diversity,
        "major_actions_last_90d": major_actions,
        "score": score
    })
 
stock_scores = (
    sector_df.groupby("symbol", group_keys=False)
             .apply(compute_stock_score)
             .reset_index()
             .sort_values("score", ascending=False)
)
 
stock_scores.head(10)

Step 4: Compute the sector score

Now we aggregate. The sector score can be something as simple as the average score across its tickers. But we can also add two helpful summary stats: how many tickers have positive drift, and how concentrated the research activity is in the top few names.


sector_score = stock_scores["score"].mean()
pct_positive_drift = (stock_scores["pt_drift_recent_sum"] > 0).mean()
 
top3_share = (
    stock_scores.sort_values("insights_last_90d", ascending=False)
               .head(3)["insights_last_90d"].sum()
    / stock_scores["insights_last_90d"].sum()
)
 
sector_summary = {
    "sector": sector_name,
    "sector_score_avg": sector_score,
    "pct_positive_pt_drift": pct_positive_drift,
    "top3_attention_share": top3_share
}
 
sector_summary
sector_score = stock_scores["score"].mean()
pct_positive_drift = (stock_scores["pt_drift_recent_sum"] > 0).mean()
 
top3_share = (
    stock_scores.sort_values("insights_last_90d", ascending=False)
               .head(3)["insights_last_90d"].sum()
    / stock_scores["insights_last_90d"].sum()
)
 
sector_summary = {
    "sector": sector_name,
    "sector_score_avg": sector_score,
    "pct_positive_pt_drift": pct_positive_drift,
    "top3_attention_share": top3_share
}
 
sector_summary
sector_score = stock_scores["score"].mean()
pct_positive_drift = (stock_scores["pt_drift_recent_sum"] > 0).mean()
 
top3_share = (
    stock_scores.sort_values("insights_last_90d", ascending=False)
               .head(3)["insights_last_90d"].sum()
    / stock_scores["insights_last_90d"].sum()
)
 
sector_summary = {
    "sector": sector_name,
    "sector_score_avg": sector_score,
    "pct_positive_pt_drift": pct_positive_drift,
    "top3_attention_share": top3_share
}
 
sector_summary
sector_score = stock_scores["score"].mean()
pct_positive_drift = (stock_scores["pt_drift_recent_sum"] > 0).mean()
 
top3_share = (
    stock_scores.sort_values("insights_last_90d", ascending=False)
               .head(3)["insights_last_90d"].sum()
    / stock_scores["insights_last_90d"].sum()
)
 
sector_summary = {
    "sector": sector_name,
    "sector_score_avg": sector_score,
    "pct_positive_pt_drift": pct_positive_drift,
    "top3_attention_share": top3_share
}
 
sector_summary

This is the final output:


{'sector': 'Technology',
 'sector_score_avg': 4.98499563638707,
 'pct_positive_pt_drift': 0.8,
 'top3_attention_share': 0.42857142857142855}
{'sector': 'Technology',
 'sector_score_avg': 4.98499563638707,
 'pct_positive_pt_drift': 0.8,
 'top3_attention_share': 0.42857142857142855}
{'sector': 'Technology',
 'sector_score_avg': 4.98499563638707,
 'pct_positive_pt_drift': 0.8,
 'top3_attention_share': 0.42857142857142855}
{'sector': 'Technology',
 'sector_score_avg': 4.98499563638707,
 'pct_positive_pt_drift': 0.8,
 'top3_attention_share': 0.42857142857142855}

In this run, the Technology basket shows a fairly constructive analyst backdrop. The average sector score comes in at about 4.98, and 80% of tickers have a positive recent price target drift, which suggests the optimism is not limited to just one or two names. In practical terms, this is what you want to see if you’re using analyst research as a sector-level sentiment layer. Upward target revisions are happening across most of the basket.

At the same time, the attention distribution is reasonably balanced. The top three tickers account for about 42.9% of total insight volume, which means analysts are still spending more time on the biggest names, but the sector score is not being driven entirely by a single mega-cap. That makes this sector-level score more trustworthy as a “broad sentiment” indicator rather than a proxy for one dominant stock.

Final Thoughts

The Analyst Insights API is useful when you stop treating analyst research as static text and start treating it as a live signal. It gives you a structured way to monitor how conviction shifts over time, which firms are actively updating their thinking, and how price targets evolve before anything shows up as a clean upgrade or downgrade event.

In this guide, we used it in three practical ways. First, to track sentiment drift and repeated framing changes on a single stock. Second, to turn a watchlist into a research-ready ranking by measuring PT drift and coverage breadth. Third, to roll that same logic up into a sector-level sentiment score that can be tracked over time.

If you’re building stock APIs or research tooling, this endpoint works best as a layer that sits between news and ratings. It helps you explain the “why” behind movement, prioritize what deserves attention, and make analyst coverage measurable instead of subjective.

FAQs

1) What is the Analyst Insights API, and how is it different from the analyst ratings API?

The Analyst Insights API provides structured access to analyst commentary, including the written insight text, firm metadata, rating context, and price targets when available. The analyst ratings API focuses more on discrete events like upgrades, downgrades, and rating changes. In practice, insights help you track how analyst thinking evolves between formal rating actions, which is useful when you need more context than a rating label.

2) Is the Analyst Insights API a real-time stock API?

It’s best described as a real-time stock research API. It delivers analyst insights as they are published, which makes it valuable for monitoring research flow and narrative shifts close to when they hit the market. It is not a price feed, but it complements real-time stock APIs by adding the “why” behind market attention.

3) How do you use analyst insights to build a stock watchlist or screener?

A simple approach is to start with a list of tickers and enrich each ticker with analyst signals such as latest rating, latest price target, recent price target drift, number of insights in the last 30–90 days, and number of unique firms publishing. You can then rank the watchlist using a transparent scoring formula that prioritizes stocks with rising targets, broader coverage, and higher research activity.

4) How can analyst insights be used for sector analysis?

You can take a set of tickers from a sector, pull insights for each ticker over a fixed time window, and compute per-stock signals like price target drift and research activity. Aggregating these signals gives a sector-level sentiment score, plus supporting stats such as the percentage of tickers with positive drift and whether research attention is concentrated in just a few mega-caps.

5) What are the best practices when building products with stock APIs and analyst insights?

Use analyst insights as a context layer rather than a standalone trading signal. Track changes over time, compare multiple firms instead of relying on a single source, and focus on measurable signals like target drift and coverage breadth. If you combine this endpoint with pricing data or news APIs, you can build workflows that explain movement, prioritize research, and surface conviction shifts earlier than ratings alone.

Quick Contact

Address

1 Campus Martius Detroit,

MI 48226, United States

Get in Touch

We love to help & we love to listen. Please fill out the form below and we’ll get back to you within a day

We love to help & we love to listen. Please fill out the form below and we’ll get back to you within a day

What data do you find interesting*