An insider trading data API turns SEC Form 4 filings into structured JSON you can query by ticker: who traded, how many shares, at what price, and under which transaction code. Corporate insiders, meaning officers, directors, and beneficial owners above ten percent, must report trades in their own company's stock within two business days. That makes Form 4 one of the few disclosure datasets that is both legally mandated and close to current. EDGAR publishes every filing for free, so the raw material has always been available. What slows people down is the format: XML documents with footnotes, derivative tables, and a single-letter coding scheme that quick scripts routinely misread. This post pulls insider transactions for a small stock watchlist with Python, decodes the transaction codes correctly, and collapses the result into a table you can screen.
What a Form 4 actually reports#
A Form 4 has a non-derivative table for direct stock transactions and a derivative table for options and similar instruments. Each row carries a transaction code, and the code matters more than the share count:
- P: open-market purchase
- S: open-market sale
- A: grant or award from the company
- M: exercise of an option
- F: shares withheld by the company to cover tax on vesting
- G: gift
There are rarer ones (D disposition, C conversion, X expiration, J other), but the six above cover the bulk of filings at large companies.
Only P and S are discretionary open-market decisions. An F row looks like a sale in the raw data, yet the insider never chose to sell anything; the company withheld shares to satisfy a tax obligation. An M is usually the first half of a pre-planned exercise-and-sell. A count of "insider sales" that includes F and M rows will overstate selling at nearly every company that pays employees in equity, which is most of them. Filtering on the code is the single highest-value cleaning step in this whole pipeline.
Pulling insider trading data for a watchlist in Python#
SiftingIO returns parsed Form 3, 4, and 5 transactions at GET /v1/fnd/stocks/{ticker}/insiders, authenticated with the X-API-Key header. One detail to notice before writing the loop: this endpoint paginates in smaller pages than the rest of the API. The default limit is 10 and the maximum is 25, so a watchlist puller has to follow meta.next_cursor instead of grabbing one big page.
import os
import requests
BASE = "https://api.sifting.io/v1"
HEADERS = {"X-API-Key": os.environ["SIFTING_KEY"]}
def insider_transactions(ticker, max_pages=4):
rows, cursor = [], None
for _ in range(max_pages):
params = {"limit": 25}
if cursor:
params["cursor"] = cursor
resp = requests.get(
f"{BASE}/fnd/stocks/{ticker}/insiders",
headers=HEADERS, params=params, timeout=10,
)
resp.raise_for_status()
payload = resp.json()
rows.extend(payload["data"])
cursor = payload["meta"]["next_cursor"]
if cursor is None:
break
return rows
watchlist = ["AAPL", "MSFT", "NVDA", "JPM"]
raw = {t: insider_transactions(t) for t in watchlist}
Four tickers at four pages of 25 is at most 16 requests, well inside the free tier's 60 requests per minute. The rows include the insider's name and role, the transaction and filing dates, the code, share count, price, and post-transaction holdings; exact field names are documented at /docs.
Collapsing filings into a screening table#
With the raw rows in hand, pandas does the rest. Filter to open-market activity first, then aggregate by ticker and direction.
import pandas as pd
frames = []
for ticker, rows in raw.items():
df = pd.DataFrame(rows)
df["ticker"] = ticker
frames.append(df)
txns = pd.concat(frames, ignore_index=True)
# Keep only discretionary open-market activity
open_market = txns[txns["code"].isin(["P", "S"])].copy()
open_market["value"] = open_market["shares"] * open_market["price"]
summary = (
open_market
.groupby(["ticker", "code"])["value"]
.agg(total="sum", trades="count")
.round(0)
)
print(summary)
The output is a per-ticker table of open-market buying and selling in dollar terms. From there the useful screens are simple: tickers where P value exceeds S value over the trailing quarter, clusters of three or more distinct insiders buying within a month, or a large P from an officer as opposed to a routine director purchase. One caveat applies to everything in this dataset: an insider purchase is a research lead. Plenty of well-informed insiders buy early and watch the price fall further, so nothing here should be read as a recommendation on any ticker.
Adjust the field names (code, shares, price) to match the documented schema if they differ; the aggregation logic stays the same.
Common pitfalls#
The page-size cap is the first one. Most SiftingIO endpoints default to 50 rows and accept up to 200, so it's natural to write limit=200 here too. The insiders endpoint caps at 25, and a loop that assumes bigger pages and never checks meta.next_cursor will quietly return a fraction of the available history. It will even look correct on tickers with little insider activity, which makes the bug easy to ship.
The second is the gap between transaction date and filing date. Insiders have two business days to file, and amendments arrive later still. If a backtest aligns an insider purchase to the price bar on its transaction date, it's trading on information that wasn't public yet. Align to the filing date for any historical study, and expect a dashboard sorted by transaction date to surface "new" trades that were actually disclosed days earlier.
The third is rate limiting at refresh time. A 50-ticker watchlist at four pages each is 200 calls per refresh, which blows through the free tier's 60 requests per minute if fired concurrently. Every response carries X-RateLimit-Remaining, and a 429 comes back with a Retry-After header saying how many seconds to wait. Checking the remaining-tokens header before a burst is cheaper than handling the failure after it.
Form 4 data rewards a small amount of care: filter to P and S, respect the filing date, and paginate honestly. With those three habits the endpoint drops into a watchlist script, a research notebook, or a nightly cron job without surprises. The free tier includes the fundamentals endpoints and 10,000 calls a month, enough to track a few dozen tickers daily. Start building free
