How to Get SEC Insider Trading Data in JavaScript & TypeScript

Updated 2026-09-15. By Theodor Nielsen, founder of Form4API.

Answer

To get SEC Form 4 insider trading data in JavaScript or TypeScript with a typed client rather than raw fetch calls, install the official npm package form4api, construct a Form4ApiClient with your API key, and call client.transactions.list(). Every response is typed, filters like exclude10b5 are named parameters instead of query strings you assemble yourself, and a plan-gated call throws a typed PlanError instead of an opaque HTTP failure.

Install & get an API key

The insider trading API parses EDGAR's Form 4 XML and joins it to ticker, CIK, and CUSIP metadata for you; the JS/TS SDK adds a typed client on top so you never hand-build the query string or parse the response yourself. Install it, create a free key from the dashboard — no credit card, 500 requests a day — and keep it out of source control by reading it from an environment variable:

npm install form4api
export FORM4API_KEY="your_api_key"   # from form4api.com/dashboard

Your first typed call

Construct a Form4ApiClient once and reuse it. It works identically in a Node script, a serverless function, or directly in a browser — the SDK is built on native fetch, no bundler-specific HTTP client required:

import { Form4ApiClient } from "form4api";

const client = new Form4ApiClient({ apiKey: process.env.FORM4API_KEY! });

// Recent open-market purchases at Apple
const buys = await client.transactions.list({ ticker: "AAPL", code: "P", perPage: 5 });
for (const txn of buys) {
  console.log(txn.insiderName, txn.insiderTitle, txn.sharesAmount, "@", txn.pricePerShare);
}

txn is a fully typed Transaction — your editor autocompletes fields like is10b5Plan, isOpenMarket, and sharesOwnedAfter instead of you reading the field list out of the REST docs and hoping the casing matches.

Filtering for the signal

Open-market purchases (transaction code P) are the highest-signal Form 4 event, because an insider generally buys for only one reason. With raw HTTP you would filter out pre-scheduled Rule 10b5-1 plan trades client-side after the fact; the SDK exposes it as a request-time parameter instead, so the filtering happens on the server and you get back exactly what you asked for:

const buys = await client.transactions.list({
  ticker: "AAPL",
  code: "P",
  exclude10b5: true,   // drop pre-scheduled 10b5-1 plan trades server-side
  perPage: 100,
});
console.log(`${buys.length} discretionary open-market buys`);

// Or use the built-in signal preset: open-market, no 10b5-1, no derivatives
const significant = await client.transactions.list({ ticker: "AAPL", significant: true });

For every transaction code, see the Form 4 guide; for why simultaneous buys matter more, see cluster buy signals.

Paginating safely

client.transactions.paginate() (and client.signals.paginate()) is an async generator that keeps requesting pages until the data runs out — or until the key's plan-gated query depth is hit (Free: 20 pages, Starter: 100, Pro and above: unlimited). That limit is never swallowed silently: it surfaces as a typed PaginationLimitError, thrown only after every page already yielded has been delivered to your loop:

import { PaginationLimitError } from "form4api";

try {
  for await (const page of client.transactions.paginate({ ticker: "AAPL" })) {
    // page: Transaction[]
  }
} catch (err) {
  if (err instanceof PaginationLimitError) {
    console.log(`Stopped after ${err.pagesYielded} pages — ${err.message}`);
    // err.cause is the original PlanError (requiredPlan, currentPlan, upgradeUrl)
  }
}

// Or stop deliberately before that limit is ever reached
for await (const page of client.transactions.paginate({ ticker: "AAPL" }, { maxPages: 10 })) {
  // page: Transaction[]
}

Check your plan's rate limits in the docs before running large backfills.

Typed error handling

This is the concrete advantage a typed client has over a plain HTTP call: instead of checking a bare status code and parsing an error body yourself, every failure mode is a distinct, importable error class. Calling an endpoint your key is not entitled to throws PlanError (HTTP 402), and it carries exactly what you need to act on it — the plan the call requires, the plan your key is currently on, and a direct upgrade link:

import { AuthError, PlanError, PaginationLimitError, RateLimitError, NotFoundError } from "form4api";

try {
  const signals = await client.signals.list();
} catch (err) {
  if (err instanceof PlanError) {
    console.log(`Upgrade to ${err.requiredPlan} (currently on ${err.currentPlan}): ${err.upgradeUrl}`);
  } else if (err instanceof RateLimitError) {
    console.log(`Retry after ${err.retryAfter}s`);
  } else if (err instanceof AuthError) {
    console.log("Invalid API key");
  } else if (err instanceof NotFoundError) {
    console.log("No matching resource");
  }
}

The same pattern covers PaginationLimitError (see above). With raw fetch, a plan-gated call is indistinguishable from any other non-2xx response until you inspect the JSON body yourself — here it is a branch on instanceof. The client also retries 5xx errors and network failures up to twice by default (configurable via maxRetries); 4xx responses, including all of the above, surface immediately without a retry.

Beyond transactions: Congress, holdings & convergence

The same client and the same API key reach the other three datasets Form4API covers — congressional STOCK Act trades, 13F-HR institutional holdings, and the insider/Congress convergence signal — as typed resources, not a separate integration:

import { Form4ApiClient, PlanError } from "form4api";

const client = new Form4ApiClient({ apiKey: process.env.FORM4API_KEY! });

// Free on every plan — only the disclosure window length is gated
const congressTrades = await client.congress.trades();

try {
  // Pro — ties an insider cluster buy to a disclosed congressional purchase
  const convergence = await client.signals.convergence();

  // Business — quarterly institutional holdings from 13F-HR
  const holdings = await client.holdings.list();

  console.log(convergence.length, "convergence hits;", holdings.length, "holdings rows");
} catch (err) {
  if (err instanceof PlanError) {
    console.log(`${err.message} — need ${err.requiredPlan}, key is on ${err.currentPlan}`);
  }
}

Congressional trades are disclosure-lagged by law — a Periodic Transaction Report can be filed up to 45 days after the trade it reports — which is a different latency profile from Form 4's roughly one-to-two-minute real-time ingestion. See insider vs congressional trading for the full honesty rules around that dataset before building on it.

Stated plainly, so a call never dead-ends in a surprise PlanError:

SDK callMin. planNote
client.transactions, client.insiders, client.companies, client.filingsFreeCore resources — full response fields on every tier
client.congress.trades()FreeDisclosure window is what tiers: 30d Free, 366d Starter, unlimited Pro+
client.insiders.summary(cik) / .scorecard(cik)ProInsider career summary and scorecard
client.congress.politicians() / .politician() / .ticker()ProPolitician profiles + per-ticker congress rollups
client.signals.convergence()ProInsider + Congress convergence signal
client.signals.list() / .paginate() / .explain() / .sentiment(), client.insiders.leaderboard()BusinessCluster signals, sentiment, and the insider leaderboard
client.form144.list(), client.holdings.list() / .managers()BusinessForm 144 notices of proposed sale + 13F-HR institutional holdings

Free is 500 requests/day with full core-endpoint access and no credit card, so you can evaluate client.transactions and client.congress.trades() before deciding whether the gated resources are worth upgrading for. See the pricing page for current plan details.

Real-time with webhooks

Polling is fine for analysis, but for alerts you want a push. client.webhooks is a typed resource for managing subscriptions from your own code, instead of only through the dashboard:

await client.webhooks.create(
  "https://your-app.example.com/form4-webhook",
  ["TransactionFiled", "ClusterBuy"],
);

const subs = await client.webhooks.list();
const events = await client.webhooks.events({ since: "2026-09-01" });
// client.webhooks.delete(subscriptionId) removes a subscription by its id from client.webhooks.list()

Payloads are signed with HMAC-SHA256 — the SDK does not verify the signature for you, so check the X-Insider-Signature header in your receiving endpoint before trusting a payload; see the webhooks guide for the full signing scheme. The Free tier includes 1 read-only webhook; creating and managing your own webhook endpoints needs a paid plan.

Full example

Putting it together — a single script that prints the recent discretionary open-market buys for a ticker, sorted by dollar value, and handles a plan-gated call cleanly if you point it at a resource your key does not have yet:

import { Form4ApiClient, PlanError } from "form4api";

const client = new Form4ApiClient({ apiKey: process.env.FORM4API_KEY! });

async function recentBuys(ticker: string, limit = 10) {
  const buys = await client.transactions.list({
    ticker,
    code: "P",
    exclude10b5: true,
    perPage: 100,
  });
  return buys
    .filter((t) => t.pricePerShare !== null)
    .sort((a, b) => (b.totalValue ?? 0) - (a.totalValue ?? 0))
    .slice(0, limit);
}

for (const t of await recentBuys("AAPL")) {
  console.log(
    `${t.transactionDate}  ${t.sharesAmount.toLocaleString()} sh  @ ${t.pricePerShare}  =  ${t.totalValue}`,
  );
}

try {
  await client.signals.convergence();
} catch (err) {
  if (err instanceof PlanError) {
    console.log(`Convergence needs ${err.requiredPlan} — see ${err.upgradeUrl}`);
  } else {
    throw err;
  }
}

Ready to build it for real? Grab a free key on the insider trading API page, or read the full reference docs.

Frequently asked questions

Is there an official JavaScript or TypeScript SDK for Form4API?

Yes. Form4API publishes an official TypeScript SDK on npm as form4api — install it with npm install form4api. It works in Node.js 18+ and any modern browser via native fetch, and every method is fully typed against the real API response shapes, so your editor autocompletes fields like insiderName, sharesAmount, and pricePerShare instead of you guessing them from the docs.

What does the SDK give me that a raw fetch call does not?

Typed responses and typed requests — filters like exclude10b5, significant, and code are named parameters instead of query-string strings you assemble by hand. It also ships a built-in paginate() async generator for walking full history, and typed errors: instead of checking a bare HTTP status code, a plan-gated call throws a PlanError carrying the plan you need, the plan your key is on, and a direct upgrade URL, so a failed call never dead-ends in an opaque failure.

How do I paginate through a large result set with the SDK?

client.transactions.paginate() and client.signals.paginate() are async generators that request successive pages until the data runs out. Query depth is plan-gated on the backend — Free 20 pages, Starter 100, Pro and above unlimited — and the SDK never swallows that limit: it throws a PaginationLimitError mid-iteration, but only after every page already yielded has been delivered to your loop, with a pagesYielded count and the original PlanError preserved as err.cause. Pass { maxPages } as the second argument to stop deliberately before that limit is ever reached.

Does the SDK cover congressional trading and institutional holdings data, not just Form 4?

Yes — client.congress, client.holdings, client.form144, and client.signals.convergence() are typed resources on the same client and the same API key as client.transactions. client.congress.trades() is reachable on every plan, at a disclosure window that gets longer as you go up tiers. Politician profiles, per-ticker congress rollups, and the convergence signal need the Pro plan; the raw Form 144 and 13F-HR holdings endpoints need Business. See the plan table below for the exact gates.

How do I handle a PlanError from the SDK?

Import PlanError from form4api and catch it like any other typed error: err.requiredPlan tells you the plan the call needs, err.currentPlan tells you what your key is on, and err.upgradeUrl is a direct link to upgrade. The SDK exports the same pattern for the other failure modes — AuthError for an invalid key, RateLimitError with a retryAfter in seconds, PaginationLimitError for a mid-pagination plan limit, and NotFoundError for a missing resource — so a caller can branch on instanceof instead of parsing an HTTP status code and a JSON error body itself.

Get more from this data

Get API key

500 free requests / day. No credit card.

Get API key

View API docs

curl / JavaScript / Python examples for every endpoint.

View API docs

See pricing

Free tier plus paid plans — join the waitlist for details.

See pricing