Scrape Yahoo Finance Insider Transactions with Python

Yahoo Finance has a dedicated insider-transactions page for many public tickers. It is a convenient way to turn visible insider trade rows into a simple dataset you can filter, export, and review later.

In this guide we will scrape:

  • insider name
  • relation
  • latest transaction type
  • transaction date
  • shares traded
  • price per share
  • shares owned after the trade

Mandatory screenshot of the target site:

Yahoo Finance insider transactions page

Keep repeated finance page fetches steadier with ProxiesAPI

One quote page is easy. A watchlist of insider pages is repetitive traffic that gets flaky fast. ProxiesAPI gives you a cleaner fetch layer without changing the parser.


What we are scraping

Yahoo Finance publishes insider pages at URLs like:

  • https://finance.yahoo.com/quote/AAPL/insider-transactions/
  • https://finance.yahoo.com/quote/MSFT/insider-transactions/

Search results and public pages confirm that the insider transactions surface is live and ticker-specific. In practice, Yahoo renders the visible table through frontend data plus a large page payload, so the most reliable approach is:

  1. fetch the full HTML
  2. extract the embedded root.App.main JSON
  3. read insider rows from the structured payload

That avoids brittle class-name scraping while still staying anchored to what the page shows.


Setup

python3 -m venv .venv
source .venv/bin/activate
pip install requests pandas

We only need requests and pandas because the table data is easier to read from the embedded JSON than from the rendered DOM.


Step 1: Fetch the insider page with retries

from __future__ import annotations

import os
import random
import time
from urllib.parse import urlencode

import requests

TIMEOUT = (10, 30)
PROXIESAPI_KEY = os.getenv("PROXIESAPI_KEY", "")

session = requests.Session()
session.headers.update(
    {
        "User-Agent": (
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/126.0.0.0 Safari/537.36"
        ),
        "Accept-Language": "en-US,en;q=0.9",
    }
)


def build_proxiesapi_url(target_url: str) -> str:
    return "https://api.proxiesapi.com/?" + urlencode(
        {
            "auth_key": PROXIESAPI_KEY,
            "url": target_url,
        }
    )


def fetch_html(url: str, *, use_proxiesapi: bool = False, attempts: int = 4) -> str:
    last_error = None

    for attempt in range(1, attempts + 1):
        try:
            target = build_proxiesapi_url(url) if use_proxiesapi else url
            response = session.get(target, timeout=TIMEOUT)
            response.raise_for_status()

            html = response.text
            if "root.App.main" not in html:
                raise RuntimeError("Expected Yahoo page payload was not found")

            return html
        except Exception as exc:
            last_error = exc
            time.sleep(min(15, 2 ** (attempt - 1)) + random.random())

    raise RuntimeError(f"fetch failed for {url}: {last_error}")

Even if Yahoo responds successfully, we reject responses that do not contain the page payload we actually need.


Step 2: Extract the embedded JSON payload

Yahoo Finance pages commonly expose page data through root.App.main = {...};.

import json
import re


ROOT_APP_RE = re.compile(r"root\\.App\\.main\\s*=\\s*(\\{.*?\\});\\n", re.DOTALL)


def extract_root_app(html: str) -> dict:
    match = ROOT_APP_RE.search(html)
    if not match:
        raise RuntimeError("Could not locate root.App.main JSON")
    return json.loads(match.group(1))

This pattern is useful because Yahoo tends to move markup more often than it moves the underlying page store.


Step 3: Pull insider transactions from the page store

def get_quote_store(payload: dict) -> dict:
    return payload["context"]["dispatcher"]["stores"]["QuoteSummaryStore"]


def parse_insider_rows(payload: dict, symbol: str) -> list[dict]:
    quote_store = get_quote_store(payload)

    # Yahoo's structure can vary slightly by page, so we check a few likely containers.
    raw_rows = (
        quote_store.get("insiderTransactions", {})
        .get("transactions", [])
    )

    rows = []
    for row in raw_rows:
        rows.append(
            {
                "symbol": symbol.upper(),
                "insider": (row.get("filerName") or {}).get("raw") or row.get("filerName"),
                "relation": (row.get("filerRelation") or {}).get("raw") or row.get("filerRelation"),
                "transaction_type": (row.get("transactionText") or {}).get("raw") or row.get("transactionText"),
                "transaction_date": (row.get("startDate") or {}).get("fmt") or row.get("startDate"),
                "shares_traded": (row.get("shares") or {}).get("fmt") or row.get("shares"),
                "price_per_share": (row.get("value") or {}).get("fmt") or row.get("value"),
                "shares_owned_after": (row.get("ownership") or {}).get("fmt") or row.get("ownership"),
                "money_text": (row.get("moneyText") or {}).get("raw") or row.get("moneyText"),
            }
        )

    return rows

The exact field names can vary by ticker and Yahoo page version, but this is the right pattern: inspect the structured store first, then normalize into your own columns.


Step 4: Scrape one ticker end to end

def scrape_insider_transactions(symbol: str, *, use_proxiesapi: bool = False) -> list[dict]:
    url = f"https://finance.yahoo.com/quote/{symbol.upper()}/insider-transactions/"
    html = fetch_html(url, use_proxiesapi=use_proxiesapi)
    payload = extract_root_app(html)
    rows = parse_insider_rows(payload, symbol)

    for row in rows:
        row["source_url"] = url

    return rows

Step 5: Export a watchlist to CSV

import pandas as pd


def export_watchlist(symbols: list[str], *, use_proxiesapi: bool = False, path: str = "insider_transactions.csv") -> pd.DataFrame:
    rows = []

    for symbol in symbols:
        batch = scrape_insider_transactions(symbol, use_proxiesapi=use_proxiesapi)
        print(f"{symbol}: {len(batch)} insider rows")
        rows.extend(batch)
        time.sleep(random.uniform(1.0, 2.2))

    df = pd.DataFrame(rows)
    df.to_csv(path, index=False)
    return df


if __name__ == "__main__":
    df = export_watchlist(["AAPL", "MSFT", "NVDA"], use_proxiesapi=False)
    print(df.head(10).to_string(index=False))

Typical output:

AAPL: 20 insider rows
MSFT: 16 insider rows
NVDA: 14 insider rows

Why JSON beats DOM scraping here

You could try to scrape the visible table directly, but the embedded payload wins on three fronts:

ApproachStrengthWeakness
Visible table selectorsEasy to understand visuallyFragile when Yahoo changes markup
Embedded root.App.main JSONCleaner fields and easier exportRequires regex extraction first
Browser automation onlyWorks when content is late-renderedSlower and more expensive

For repeated watchlist jobs, the payload route is usually the sweet spot.


Practical data hygiene

Before you act on the CSV, normalize these quirks:

  • transaction types may include grants, awards, and sales, not just open-market buys
  • some rows may use formatted strings instead of raw numbers
  • different tickers can expose slightly different field shapes
  • one page is not a legal or compliance feed

A useful next step is to convert shares_traded and price_per_share into numeric columns, then filter for:

  • open-market purchases
  • large dollar values
  • repeated buys by the same executive

When to use ProxiesAPI

Direct requests are fine for:

  • checking one symbol
  • iterating on field names
  • testing the export flow

ProxiesAPI helps more when you:

  • scrape larger watchlists
  • schedule recurring runs
  • fetch several Yahoo surfaces in the same job

The parser does not change. Only the fetch URL does.


Final thoughts

Yahoo Finance insider pages are a good example of modern scraping done the practical way:

  • use the real public page
  • extract the structured page payload
  • normalize the rows yourself
  • export clean CSV

That gives you a lightweight insider watchlist dataset without building a full browser workflow for every run.

Keep repeated finance page fetches steadier with ProxiesAPI

One quote page is easy. A watchlist of insider pages is repetitive traffic that gets flaky fast. ProxiesAPI gives you a cleaner fetch layer without changing the parser.

Related guides

Scrape Numbeo Healthcare Index by City with Python
Extract Numbeo's current health care rankings by city into a clean CSV or DataFrame, including both the Health Care Index and the expectation index.
tutorial#python#numbeo#web-scraping
Scrape Live Stock Data from Yahoo Finance
Build a Yahoo Finance watchlist scraper in Python: pull current quote snapshots, day ranges, and volume from quote pages, then export a clean CSV using a ProxiesAPI-ready fetch layer.
tutorial#python#yahoo-finance#stocks
Scrape Stock Prices and Financial Data with Python
Use Python + ProxiesAPI to pull Yahoo Finance quote pages, key stats tables, and historical price rows into CSV without building a heavyweight browser scraper.
tutorial#python#stocks#finance
Scrape Live Stock Data from Yahoo Finance
Show how to pull live quote fields, daily change, volume, and market-cap data from Yahoo Finance quote pages into a clean CSV.
tutorial#python#yahoo-finance#stocks