DEV Community

Caleb Love
Caleb Love

Posted on

How to pull SEC filings, insider trades and XBRL financials into your app in 5 minutes

If you've ever tried to build something on top of SEC data, you know the feeling. EDGAR has everything: every 10-K, every 8-K, every Form 4 insider trade, and years of XBRL financials for U.S. public companies. It's free and public. But getting from "I want Apple's recent insider sales" to a clean JSON array takes a surprising amount of glue code.

I built a small API to skip that glue. This post shows what it does, how to call it in a few minutes, and four real use cases with copy-paste examples. All the data comes from SEC EDGAR's public data. The API isn't affiliated with the SEC, and nothing here is investment advice.

What it does

The SEC EDGAR Insider Trading & Financials API runs on Cloudflare Workers and exposes these endpoints:

Endpoint What you get
GET /v1/filings/{tickerOrCik} Company profile + filings (10-K, 10-Q, 8-K with item numbers, S-1, 4…) with document URLs. Filters: forms, since, until, limit
GET /v1/insider-trades/{tickerOrCik} Parsed Form 4 transactions, one row per trade, with buy/sell totals. Filters: limit, side, codes, since, includeDerivative
GET /v1/insider-buys Market-wide open-market insider purchases grouped by company, with cluster-buy detection. Filters: days, minInsiders, minValue, role, limit
GET /v1/company/{tickerOrCik}/snapshot Revenue, margins, net income, EPS, cash flow, balance sheet and debt, with YoY growth
GET /v1/company/{tickerOrCik}/facts Raw XBRL companyfacts, passed through
GET /v1/company/{tickerOrCik}/concept/{taxonomy}/{tag} History of a single XBRL tag, e.g. us-gaap/NetIncomeLoss
GET /v1/search EDGAR full-text search (q, forms, ciks, startdt, enddt, from)
GET /v1/resolve/{tickerOrCik} Ticker ↔ CIK

You can pass a ticker (AAPL, BRK.B) or a CIK (320193) anywhere an ID is expected.

Quick start

The /v1 endpoints are served through RapidAPI. Calling the Worker directly gets you a 403 that points you to the listing.

  1. Open the RapidAPI listing and subscribe to the free Basic plan.
  2. Copy your key from the RapidAPI dashboard.
  3. Make a call:
curl "https://sec-edgar-insider-trading-financials-api.p.rapidapi.com/v1/resolve/NVDA" \
  -H "X-RapidAPI-Key: YOUR_RAPIDAPI_KEY" \
  -H "X-RapidAPI-Host: sec-edgar-insider-trading-financials-api.p.rapidapi.com"
Enter fullscreen mode Exit fullscreen mode
{ "cik": "0001045810", "ticker": "NVDA", "name": "NVIDIA CORP" }
Enter fullscreen mode Exit fullscreen mode

For the rest of the post I'll use this setup:

export RAPIDAPI_KEY=YOUR_RAPIDAPI_KEY
export HOST=sec-edgar-insider-trading-financials-api.p.rapidapi.com
Enter fullscreen mode Exit fullscreen mode

You can check that the backend is up without a key: https://sec-edgar-api.kidriahx.workers.dev/health returns {"ok": true, ...}, and the OpenAPI spec is at /openapi.yaml.

Use case 1: Latest 10-K and 8-K filings for a company

Say you want a "recent filings" panel on a stock page.

curl "https://$HOST/v1/filings/MSFT?forms=10-K,8-K&limit=3" \
  -H "X-RapidAPI-Key: $RAPIDAPI_KEY" -H "X-RapidAPI-Host: $HOST"
Enter fullscreen mode Exit fullscreen mode

Response (trimmed):

{
  "company": {
    "cik": "0000789019",
    "name": "MICROSOFT CORP",
    "tickers": ["MSFT"],
    "sicDescription": "Services-Prepackaged Software",
    "fiscalYearEnd": "0630"
  },
  "count": 3,
  "filings": [
    {
      "accessionNumber": "0001193125-26-380280",
      "form": "8-K",
      "filingDate": "2026-09-02",
      "items": "7.01,9.01",
      "documentUrl": "https://www.sec.gov/Archives/edgar/data/789019/000119312526380280/d291965d8k.htm"
    },
    {
      "accessionNumber": "0001193125-26-323660",
      "form": "10-K",
      "filingDate": "2026-07-29",
      "reportDate": "2026-06-30",
      "documentUrl": "https://www.sec.gov/Archives/edgar/data/789019/000119312526323660/msft-20260630.htm"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The items field on 8-Ks is useful: 2.02 is results of operations (earnings), 5.02 is officer/director changes, and so on. You can filter on it without opening the document.

Use case 2: Insider trades for a ticker (Form 4)

Form 4s are XML documents, and every filing can hold several transactions. The API parses them into flat rows.

JavaScript (fetch, Node 18+ or browser):

const HOST = "sec-edgar-insider-trading-financials-api.p.rapidapi.com";

async function insiderTrades(ticker) {
  const res = await fetch(`https://${HOST}/v1/insider-trades/${ticker}?limit=5`, {
    headers: {
      "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, // YOUR_RAPIDAPI_KEY
      "X-RapidAPI-Host": HOST,
    },
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

const data = await insiderTrades("AAPL");
console.log(data.summary);
for (const t of data.transactions.filter((t) => t.side === "sell")) {
  console.log(t.transactionDate, t.insiderName, t.shares, t.pricePerShare, t.is10b51Plan);
}
Enter fullscreen mode Exit fullscreen mode

Python (requests):

import os
import requests

HOST = "sec-edgar-insider-trading-financials-api.p.rapidapi.com"
HEADERS = {
    "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],  # YOUR_RAPIDAPI_KEY
    "X-RapidAPI-Host": HOST,
}

r = requests.get(f"https://{HOST}/v1/insider-trades/AAPL",
                 params={"limit": 5}, headers=HEADERS, timeout=30)
r.raise_for_status()
data = r.json()
print(data["summary"])
for t in data["transactions"]:
    if t["side"] == "sell":
        print(t["transactionDate"], t["insiderName"], t["shares"], t["pricePerShare"])
Enter fullscreen mode Exit fullscreen mode

Response (trimmed):

{
  "query": { "id": "AAPL", "cik": "0000320193", "ticker": "AAPL", "name": "Apple Inc." },
  "filingsParsed": 5,
  "summary": {
    "transactions": 23,
    "openMarketBuys": 0,
    "openMarketSells": 13,
    "openMarketSellShares": 9869,
    "openMarketSellValue": 3361318.27,
    "distinctInsiders": 3
  },
  "transactions": [
    {
      "insiderName": "Newstead Jennifer",
      "insiderRole": "SVP, GC and Government Affairs",
      "transactionDate": "2026-10-20",
      "transactionCode": "S",
      "transactionType": "Open-market or private sale",
      "side": "sell",
      "shares": 2399,
      "pricePerShare": 339.62,
      "totalValue": 814748.38,
      "sharesOwnedAfter": 34795,
      "is10b51Plan": true,
      "filingUrl": "https://www.sec.gov/Archives/edgar/data/320193/000114036126040540/form4.xml"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Use side=buy or side=sell to keep only open-market purchases (code P) or sales (code S). Use codes=P,S,M for finer control.

Use case 3: Market-wide cluster buys

When several insiders buy the same stock in the open market within a short window, people call it a "cluster buy." The API keeps a rolling store of recent Form 4 purchases (it scans EDGAR every 10 minutes) and groups them by company:

curl "https://$HOST/v1/insider-buys?days=7&minInsiders=2&limit=5" \
  -H "X-RapidAPI-Key: $RAPIDAPI_KEY" -H "X-RapidAPI-Host: $HOST"
Enter fullscreen mode Exit fullscreen mode
r = requests.get(f"https://{HOST}/v1/insider-buys",
                 params={"days": 7, "minInsiders": 2, "limit": 5},
                 headers=HEADERS, timeout=30)
for c in r.json()["results"]:
    print(c["ticker"], c["insiderCount"], c["totalValue"])
Enter fullscreen mode Exit fullscreen mode

Response (trimmed, from the morning of Oct 24, 2026):

{
  "window": { "days": 7, "from": "2026-10-17", "to": "2026-10-24" },
  "clusterBuys": 10,
  "results": [
    {
      "ticker": "RAPP",
      "issuerName": "Rapport Therapeutics, Inc.",
      "insiderCount": 6,
      "isClusterBuy": true,
      "totalShares": 61694,
      "totalValue": 1664824.35,
      "avgPrice": 26.9852,
      "firstTransactionDate": "2026-10-21",
      "lastTransactionDate": "2026-10-23",
      "insiders": [
        { "name": "PAUL STEVEN M", "role": "Director", "value": 974277.92 },
        { "name": "Ignelzi Troy A.", "role": "Chief Financial Officer", "value": 143550 }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

One caveat: this feed only covers filings since the scanner started running, so it's a recent-activity feed, not a historical database.

Use case 4: Financial snapshot from XBRL

Pulling "revenue" from XBRL sounds simple, but companies use different tags (Revenues, RevenueFromContractWithCustomerExcludingAssessedTax, …), and annual and quarterly facts are mixed together. The snapshot endpoint picks the tags and lines up the periods for you:

const snap = await fetch(`https://${HOST}/v1/company/AAPL/snapshot`, {
  headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": HOST },
}).then((r) => r.json());

const rev = snap.metrics.revenue;
console.log(rev.latestAnnual.value, rev.annualGrowthPct);
Enter fullscreen mode Exit fullscreen mode
{
  "company": { "cik": "0000320193", "ticker": "AAPL", "name": "Apple Inc.", "fiscalYearEnd": "0926" },
  "metrics": {
    "revenue": {
      "tag": "us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax",
      "unit": "USD",
      "latestAnnual": { "value": 416161000000, "periodEnd": "2025-09-27", "fiscalYear": 2025, "form": "10-K" },
      "priorAnnual": { "value": 391035000000, "periodEnd": "2024-09-28" },
      "annualGrowthPct": 6.43,
      "latest": { "value": 109417000000, "periodEnd": "2026-06-27", "fiscalPeriod": "Q3", "form": "10-Q" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Each metric includes the exact XBRL tag and accession number, so you can trace any number back to the filing. If you need a metric that isn't in the snapshot, use /v1/company/AAPL/concept/us-gaap/NetIncomeLoss.

Bonus: full-text search. /v1/search?q="share repurchase"&forms=8-K wraps EDGAR full-text search. When I ran it, it reported 5,661 matching 8-Ks from the past year, returned 100 per page.

Why not just call EDGAR directly?

You can, and for a one-off script you probably should. The SEC's APIs are free. Here's what this API handles for you:

  • User-Agent rules. SEC asks every client to declare who it is (a name and contact email) in the User-Agent header. Requests without one can be blocked.
  • Rate limits. SEC's fair-access limit is 10 requests per second. Parsing insider trades means one request per Form 4, and a snapshot uses several XBRL lookups. The API spaces out its SEC calls globally and caches results.
  • CIK lookups. Most SEC endpoints want a zero-padded CIK, not a ticker.
  • Messy formats. Form 4s are XML with separate derivative and non-derivative tables. The submissions JSON stores filings as parallel arrays. companyfacts can run to megabytes per company, with duplicate facts across filings.

If you only need one company's 10-K list, go direct. If you're building a feature on top of insider trades or fundamentals, a wrapper saves you a few evenings.

Pricing

RapidAPI has four plans. Basic is free and covers every endpoint, which is enough to try things out. Paid tiers add more requests per month. Current quotas and prices are on the pricing page.

If you'd rather work in batches (pull every 8-K in a date range into a dataset, or schedule a run), the same kind of data is also available as an Apify actor: SEC EDGAR Filings & Financials on Apify. It's billed per result.

Links

If you build something with it, or hit a filing it parses wrong, let me know in the comments. Edge cases in Form 4s are endless, and I'd like to fix them.

— Caleb Love

Top comments (0)