SEC Form 4 insider trading API
Five read-only JSON endpoints over U.S. corporate insider disclosures: the filing feed itself, insider identity search, one insider's transaction history, and the cluster and streak digests built on top of them. No key is needed to read, at 60 requests per hour per IP. A free key raises that to 600. Everything is refreshed hourly and every row links back to the original EDGAR document.
Tracefour displays what was filed. It does not rate, rank for you, or predict. Not investment advice.
Quick start
The 50 most recent open-market insider purchases above one million dollars. This works right now, from a terminal, with nothing configured.
curl "https://tracefour.com/v1/filings?direction=P&min_value=1000000&limit=50" Every response uses the envelope { data, error, meta }. Rate limits, error codes and the optional key are documented on the API overview. Machine-readable spec: /v1/openapi.json.
Read this before you request an insider
Insider slugs are EDGAR name order, surname first, taken from the name the filing agent submitted. Jensen Huang files as HUANG JEN HSUN, so the slug is huang-jen-hsun. Stephen Edward Hull, who files at Leidos Holdings, appears as Hull Stephen Edward, so the slug is hull-stephen-edward, never stephen-hull. Middle initials, suffixes and the issuer's own spelling all make it into the string.
There is no transformation from a person's common name to that slug, so do not build one. Search, read the slug back, then request it.
# Wrong. A slug built from a person's common name, forename first, 404s.
# There is no rule that turns "Firstname Lastname" into a real slug.
curl -s -o /dev/null -w '%{http_code}\n' "https://tracefour.com/v1/insiders/forename-surname"
# 404
# Right. Search for the name, read the slug back, then request it.
curl "https://tracefour.com/v1/insiders?q=huang" | jq -r '.data[].slug'
# chuang-tzu-sou
# huang-jen-hsun
curl "https://tracefour.com/v1/insiders/huang-jen-hsun" Tickers are the opposite: /v1/filings?ticker=NVDA takes the plain symbol, 1 to 5 letters, case-insensitive.
Transaction codes, and where each one appears
A Form 4 reports several kinds of event under one form, and only two of them describe an insider choosing to buy or sell on the market. Every feed on this page returns those two and nothing else, whatever direction is set to. The rest are still public, and still ours to show, just not as market activity: they come back from GET /v1/insiders/{id}, which reports one person's complete filed history.
| Code | What the SEC code means | Where it appears |
|---|---|---|
P | Open-market or private purchase. The insider paid for the shares. | Every endpoint on this page. This is direction=P. |
S | Open-market or private sale. The insider sold shares. | Every endpoint on this page. This is direction=S. |
A | Grant, award or other acquisition from the issuer. No purchase decision was made. | Excluded from the four feeds. Returned by /v1/insiders/{id} only. |
M | Exercise or conversion of a derivative security. Mechanical, not directional. | Excluded from the four feeds. Returned by /v1/insiders/{id} only. |
F | Shares withheld by the issuer to cover tax on a vest. | Excluded from the four feeds. Returned by /v1/insiders/{id} only. |
G | Bona fide gift. | Excluded from the four feeds. Returned by /v1/insiders/{id} only. |
D | Disposition to the issuer, for example a share buyback tender. | Excluded from the four feeds. Returned by /v1/insiders/{id} only. |
J | Other acquisition or disposition. The filer explains it in a footnote. | Excluded from the four feeds. Returned by /v1/insiders/{id} only. |
Endpoints
GET /v1/filings
Open-market Form 4 transactions from the rolling 365-day window, newest filing first. One row per reported transaction, not per filing: a Form 4 that reports three lots produces three rows, each with its own filingId suffix.
This is the open-market feed. Every row is a purchase (P) or a sale (S), and the default direction=all means both of those, not every Form 4 code. Awards, option exercises, tax withholding, gifts and conversions carry no decision to trade on the market and are excluded here, as they are from the cluster and streak digests. To see them for one person, call /v1/insiders/{id}, which returns their complete filed history.
since is clamped to the 365-day retention horizon, so an older date means the whole window rather than an error. Filtering and the limit are applied in SQL, so a wide since with a small limit is cheap.
Rows are deduplicated before they reach you. Joint beneficial ownership makes several people file the same economic lot, and a filer occasionally resubmits an identical filing under a fresh accession number. One canonical row survives per economic lot. A measured $1.23 billion of double counting was removed by that pass in August 2026.
| Parameter | Type | Accepts | Default |
|---|---|---|---|
since | string | YYYY-MM-DD. Clamped to the 365-day retention horizon. | 30 days ago |
direction | string | P for purchases, S for sales, or all for both. There is no value that returns non-open-market codes. | all |
min_value | number | Minimum totalValue in USD. 0 or greater. | 0 |
ticker | string | 1 to 5 letters, case-insensitive, uppercased server-side. | none |
limit | integer | 1 to 500. | 100 |
curl
curl "https://tracefour.com/v1/filings?direction=P&min_value=1000000&since=2026-06-01&limit=2"Python
import requests
response = requests.get(
"https://tracefour.com/v1/filings",
params={"direction": "P", "min_value": 1_000_000, "since": "2026-06-01", "limit": 2},
timeout=30,
)
response.raise_for_status()
for row in response.json()["data"]:
print(row["transactionDate"], row["issuerTicker"], row["ownerName"], row["totalValue"])JavaScript
const url = new URL("https://tracefour.com/v1/filings")
url.search = new URLSearchParams({
direction: "P",
min_value: "1000000",
since: "2026-06-01",
limit: "2",
}).toString()
const { data, meta } = await fetch(url).then((response) => response.json())
console.log(meta.count, data[0].issuerTicker, data[0].totalValue)Response
Captured from the curl above. Both rows are shown as one: the second row is identical in shape. Every field the endpoint returns is present here, and the meta block is byte-for-byte what every /v1 response carries, so later blocks on this page trim it to its first three keys.
{
"data": [
{
"cik": "1142417",
"filedAt": "2026-06-29T17:55:16.000Z",
"filingId": "0001193125-26-288030-0",
"formType": null,
"originalSubmissionDate": null,
"periodOfReport": null,
"hasAdsFootnote": false,
"hasIndirectOwnershipFootnote": false,
"hasTaxWithholdingFootnote": false,
"hasVwapFootnote": false,
"is10b5_1": false,
"isDirector": true,
"isOfficer": true,
"isOther": false,
"isPairedExercise": false,
"isTenPercentOwner": false,
"otherText": null,
"issuerName": "NEXSTAR MEDIA GROUP, INC.",
"issuerTicker": "NXST",
"ownerName": "SOOK PERRY A",
"ownerTitle": "Chief Executive Officer",
"ownershipType": "D",
"pricePerShare": 162.2647,
"rawFilingUrl": "https://www.sec.gov/Archives/edgar/data/1250261/000119312526288030/0001193125-26-288030-index.htm",
"shares": 12235,
"sharesOwnedAfter": 899044,
"totalValue": 1985308.6045000001,
"transactionCode": "P",
"transactionDate": "2026-06-26",
"url": "https://tracefour.com/insider-trading/NXST"
}
],
"meta": {
"count": 2,
"fetchedAt": "2026-09-09T18:54:41.158Z",
"source": "sec-edgar",
"attribution": {
"text": "Underlying filings are public-domain government works. Tracefour compilation licensed CC BY 4.0: when you display it, link to the Tracefour page in meta.attribution.page, or the url on each record. Not investment advice.",
"site": "https://tracefour.com",
"page": "https://tracefour.com/filings"
},
"license": "https://creativecommons.org/licenses/by/4.0/",
"sourceLicense": "https://www.usa.gov/government-works"
}
}GET /v1/insiders?q=
Resolve a person name to the insider slug the rest of the API expects. Matches on both the slug and the filed name, and returns the tickers that person has filed against plus the date they were last seen.
Call this before you call /v1/insiders/{id}. Slugs come from the name as EDGAR received it, which is legal name order, surname first. Jensen Huang files as HUANG JEN HSUN, so his slug is huang-jen-hsun. There is no rule that turns a person name into that string, which is why this endpoint exists.
A slug survives its filings. Identity rows are never pruned, so a search hit always resolves even when the person last filed outside the 365-day transaction window.
| Parameter | Type | Accepts | Default |
|---|---|---|---|
q | string | 1 to 64 characters. Matched against slug and filed name. | required |
limit | integer | 1 to 100. | 20 |
curl
curl "https://tracefour.com/v1/insiders?q=huang&limit=3"Python
import requests
response = requests.get(
"https://tracefour.com/v1/insiders",
params={"q": "huang", "limit": 3},
timeout=30,
)
response.raise_for_status()
for hit in response.json()["data"]:
print(hit["slug"], "|", hit["name"], "|", hit["tickers"])JavaScript
const url = new URL("https://tracefour.com/v1/insiders")
url.search = new URLSearchParams({ q: "huang", limit: "3" }).toString()
const { data } = await fetch(url).then((response) => response.json())
const nvidiaChief = data.find((hit) => hit.tickers.includes("NVDA"))
console.log(nvidiaChief.slug)Response
Captured from the curl above. Two hits were returned for "huang" against the local dataset; the search is a substring match, so "chuang-tzu-sou" is a legitimate hit. meta trimmed to its first three keys.
{
"data": [
{
"slug": "chuang-tzu-sou",
"name": "Chuang Tzu-Sou",
"tickers": ["TSM"],
"lastSeen": "2026-06-24T10:43:03.000Z"
},
{
"slug": "huang-jen-hsun",
"name": "HUANG JEN HSUN",
"tickers": ["NVDA"],
"lastSeen": "2026-06-23T10:00:38.000Z"
}
],
"meta": {
"count": 2,
"fetchedAt": "2026-09-09T18:22:25.648Z",
"source": "sqlite"
}
}GET /v1/insiders/{id}
One insider: the identity row, their transactions inside the rolling window, their archived career lots, and the people who co-filed the same economic lots with them.
identity.lifetimeFilings counts every transaction row ever ingested for that person, so it keeps climbing after older rows leave the window. identity.lifetimeDistinctFilings counts the underlying documents instead.
This is the one endpoint that returns every transaction code, not just P and S. A person’s history includes their awards, vests and exercises, and omitting them would misreport what they filed. The captured response below opens on an F row, tax withheld on a vest, for exactly that reason.
Transactions here carry two fields the list endpoint does not. intentLabel is a plain-language reading of the transaction code, for example "Tax withholding". signalQuality is one of exactly six string literals: discretionary-buy, discretionary-sell, mechanical-buy, mechanical-sell, non-discretionary, planned. Note the direction suffix on the first four, so an equality test against "discretionary" matches nothing. A 10b5-1 sale was scheduled in advance and comes back as planned, which records the fact rather than interpreting it.
An unknown slug returns 404 and a slug that fails the kebab-case shape returns 400. Resolve the slug with /v1/insiders?q= rather than constructing it.
| Parameter | Type | Accepts | Default |
|---|---|---|---|
id | string (path) | Kebab-case ASCII, 1 to 160 characters. EDGAR name order. | required |
curl
curl "https://tracefour.com/v1/insiders/huang-jen-hsun"Python
import requests
response = requests.get("https://tracefour.com/v1/insiders/huang-jen-hsun", timeout=30)
response.raise_for_status()
payload = response.json()["data"]
print(payload["identity"]["ownerName"], payload["identity"]["lifetimeFilings"])
print(len(payload["transactions"]), "transactions in the rolling window")JavaScript
const response = await fetch("https://tracefour.com/v1/insiders/huang-jen-hsun")
if (response.status === 404) throw new Error("No such insider slug. Search first with /v1/insiders?q=")
const { data } = await response.json()
console.log(data.identity.ownerName, data.transactions.length)Response
Captured from the curl above. transactions held 217 rows; one is shown. careerLots and coFilers were empty for this insider and are shown as returned. meta trimmed to its first three keys.
{
"data": {
"identity": {
"slug": "huang-jen-hsun",
"ownerName": "HUANG JEN HSUN",
"tickers": ["NVDA"],
"firstSeen": "2025-07-07T00:00:00.000Z",
"lastSeen": "2026-06-23T10:00:38.000Z",
"lifetimeFilings": 217,
"lifetimeDistinctFilings": 15,
"directionalFilings": 215
},
"transactions": [
{
"filingId": "0001197649-26-000010-0",
"filedAt": "2026-06-23T10:00:38.000Z",
"issuerName": "NVIDIA CORP",
"issuerTicker": "NVDA",
"ownerName": "HUANG JEN HSUN",
"ownerTitle": "President and CEO",
"transactionCode": "F",
"transactionDate": "2026-06-17",
"shares": 45723,
"pricePerShare": 207.41,
"totalValue": 9483407.43,
"sharesOwnedAfter": 70146252,
"intentLabel": "Tax withholding",
"signalQuality": "non-discretionary"
}
],
"careerLots": [],
"coFilers": []
},
"meta": {
"count": 217,
"fetchedAt": "2026-09-09T18:23:09.493Z",
"source": "sqlite"
}
}GET /v1/clusters
Cluster digests: tickers where 3 or more distinct insiders filed in the same direction inside a 60-day window. Up to 24 cards, built from open-market P and S rows only.
An insider counts toward a cluster once their open-market total in the window reaches $10,000, so a handful of employee fills cannot pad the headcount. Each card carries issuerTicker, direction, insiderCount, totalValue, discretionaryValue (the same total with 10b5-1 plan trades removed), a seniority breakdown, first and last filing dates, and a conviction number between 0 and 100.
Conviction is a published arithmetic composite of breadth, seniority, materiality against shares outstanding, recency and concentration. The formula is written out on the about page. It describes what was filed. It is not a forecast and not advice.
The array is empty whenever no ticker meets the threshold in the current window, which is a normal state, not an error. Check meta.count.
No parameters.
curl
curl "https://tracefour.com/v1/clusters"Python
import requests
response = requests.get("https://tracefour.com/v1/clusters", timeout=30)
response.raise_for_status()
for card in response.json()["data"]:
print(card["issuerTicker"], card["direction"], card["insiderCount"], card["conviction"])JavaScript
const { data, meta } = await fetch("https://tracefour.com/v1/clusters").then((r) => r.json())
console.log(`${meta.count} active clusters`)
for (const card of data) console.log(card.issuerTicker, card.insiderCount, card.url)Response
Captured verbatim, including the empty array: no cluster met the 60-day threshold in the dataset this was captured against. The meta block is what you get on every call, and the data array holds one card object per cluster with the fields named above.
{
"data": [],
"meta": {
"count": 0,
"fetchedAt": "2026-09-09T18:30:20.799Z",
"source": "sqlite",
"attribution": {
"text": "Underlying filings are public-domain government works. Tracefour compilation licensed CC BY 4.0: when you display it, link to the Tracefour page in meta.attribution.page, or the url on each record. Not investment advice.",
"site": "https://tracefour.com",
"page": "https://tracefour.com/clusters"
},
"license": "https://creativecommons.org/licenses/by/4.0/",
"sourceLicense": "https://www.usa.gov/government-works"
}
}GET /v1/streaks
Streak digests: insiders who filed in the same direction for two or more consecutive weeks, up to 50 runs, built from open-market P and S rows only.
A run qualifies once its summed value across the consecutive weeks reaches $50,000. Each entry carries ownerName, ownerSlug, issuerTicker, direction, weeks, streakValue, plannedValue (the 10b5-1 share of that total), a twelve-week filing history, a daily price series, and a track record of the person’s previous completed runs.
The price series is split-adjusted end to end, so the percentage attached to a run is computed in one share unit rather than across a split boundary.
The array is empty when no run is active, in the same way /v1/clusters is. Check meta.count.
No parameters.
curl
curl "https://tracefour.com/v1/streaks"Python
import requests
response = requests.get("https://tracefour.com/v1/streaks", timeout=30)
response.raise_for_status()
for run in response.json()["data"]:
print(run["ownerName"], run["issuerTicker"], run["weeks"], "weeks", run["streakValue"])JavaScript
const { data, meta } = await fetch("https://tracefour.com/v1/streaks").then((r) => r.json())
console.log(`${meta.count} active streaks`)
for (const run of data) console.log(run.ownerSlug, run.weeks, run.direction)Response
Captured verbatim, including the empty array: no run was active in the dataset this was captured against. Note that attribution.page points at the streaks page here, as it does at the matching page on every endpoint.
{
"data": [],
"meta": {
"count": 0,
"fetchedAt": "2026-09-09T18:30:20.950Z",
"source": "sqlite",
"attribution": {
"text": "Underlying filings are public-domain government works. Tracefour compilation licensed CC BY 4.0: when you display it, link to the Tracefour page in meta.attribution.page, or the url on each record. Not investment advice.",
"site": "https://tracefour.com",
"page": "https://tracefour.com/streaks"
},
"license": "https://creativecommons.org/licenses/by/4.0/",
"sourceLicense": "https://www.usa.gov/government-works"
}
}License and attribution
Form 4 filings are public-domain U.S. government works and Tracefour claims nothing over them. What is licensed is the compilation: the parsing, split-adjustment, deduplication and scoring that turn 50,000 XML documents into these rows. That compilation is CC BY 4.0, and its one condition is attribution with a link. Use meta.attribution.page, or the url on each row.
Related
- Congress trading API: STOCK Act disclosures and tracked portfolios.
- European insider trading API: BaFin, FCA, Finansinspektionen and AFM filings.
- MCP server: the same data as nine tools for a tool-calling agent.
Frequently asked questions
Is there a free SEC Form 4 API?
Yes. GET https://tracefour.com/v1/filings returns Form 4 insider transactions as JSON with no key and no sign-up, rate-limited to 60 requests per hour per IP address. A free API key raises that to 600 requests per hour. There is no paid API tier and the filing data is not behind a price.
How do I find an insider slug for the API?
Call GET /v1/insiders?q=<name>. Slugs are built from the name as EDGAR received it, which is legal name order with the surname first: Jensen Huang files as HUANG JEN HSUN and his slug is huang-jen-hsun. A slug cannot be derived from a common name spelling, so search first and read the slug back before requesting /v1/insiders/{id}.
What do the transaction codes P and S mean on a Form 4?
P is an open-market or private purchase, where the insider paid for the shares. S is an open-market or private sale. They are the two codes that reflect a decision to trade. A is a grant or award from the issuer, M is a derivative exercise or conversion, F is share withholding to cover tax on a vest, and G is a gift.
Does the API include stock awards and option exercises?
Not in the feeds. GET /v1/filings, /v1/clusters and /v1/streaks return open-market purchases (P) and sales (S) only, and direction=all on the filings feed means both of those rather than every Form 4 code. Awards, option exercises, tax withholding, gifts and conversions are excluded because they reflect no decision to trade on the market. They are returned by GET /v1/insiders/{id}, which is one person’s complete filed history rather than a feed.
How far back does the Form 4 data go?
Transactions are kept for a rolling 365 days and the since parameter is clamped to that horizon. Identity records are never pruned, so GET /v1/insiders/{id} keeps answering for a person whose filings have aged out, returning their identity with an empty transaction list.
Can I republish Tracefour Form 4 data in my own product?
Yes. The underlying filings are public-domain U.S. government works. The Tracefour compilation of them, meaning the normalization, split-adjustment, deduplication and scoring, is licensed CC BY 4.0, whose single condition is attribution with a link. Every response names the page to link in meta.attribution.page, and each filing row carries its own url.