Node.js SEC EDGAR API examples

Working Node.js code for the most common SEC EDGAR queries: insider trades, institutional holdings, real-time 8-K events via webhooks. Uses native fetch, no SDK install required.

Want this data via API instead of reading about it? Get a free API key →

The problem

You need SEC filings in Node.js. You don't want to write XML parsers, deal with the SEC's rate limits and User-Agent requirements, or pull in a chunky SDK.

EdgarKit works with native fetch (Node 18+). The whole integration is two lines of plumbing plus the query itself.

The approach

We'll cover five common queries, each one a complete working snippet:

  1. Recent Form 4 insider purchases
  2. All filings for a specific ticker
  3. Cluster buy detection
  4. 13F holdings for a manager
  5. Real-time 8-K webhooks with HMAC verification in Express

Setup

Node 18+ has fetch built in. For older versions, install node-fetch. For the webhook server example, install Express:

npm install express

Set your API key as an environment variable:

export EDGARKIT_API_KEY="your_api_key_here"

Example 1: Recent Form 4 insider purchases

const API_KEY = process.env.EDGARKIT_API_KEY;

const url = new URL("https://api.edgarkit.com/v1/filings");
url.searchParams.set("form_type", "4");
url.searchParams.set("transaction_code", "P");
url.searchParams.set("min_value", "100000");
url.searchParams.set("limit", "20");

const resp = await fetch(url, {
  headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const { data: filings } = await resp.json();

for (const f of filings) {
  const value = Number(f.total_value).toLocaleString(undefined, {
    style: "currency",
    currency: "USD",
    maximumFractionDigits: 0,
  });
  console.log(
    `${f.issuer_ticker.padEnd(6)} ` +
    `${f.reporter_name.slice(0, 30).padEnd(30)} ` +
    `${value.padStart(15)}  ${f.transaction_date}`
  );
}

Example 2: All filings for a specific ticker

const url = new URL("https://api.edgarkit.com/v1/filings");
url.searchParams.set("ticker", "NVDA");
url.searchParams.set("limit", "50");

const resp = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.EDGARKIT_API_KEY}` },
});
const { data: filings } = await resp.json();

for (const f of filings) {
  console.log(`${f.form_type.padEnd(10)} ${f.filed_at}  accession=${f.accession_number}`);
}

Example 3: Cluster buy detection

A multi-step example. Pull recent open-market purchases above $50k from the last 14 days, group by issuer, surface clusters of 3+ distinct buyers:

async function findClusters() {
  const since = new Date();
  since.setDate(since.getDate() - 14);

  const url = new URL("https://api.edgarkit.com/v1/filings");
  url.searchParams.set("form_type", "4");
  url.searchParams.set("transaction_code", "P");
  url.searchParams.set("min_value", "50000");
  url.searchParams.set("since", since.toISOString().slice(0, 10));
  url.searchParams.set("limit", "1000");

  const resp = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.EDGARKIT_API_KEY}` },
  });
  const { data: filings } = await resp.json();

  const byIssuer = new Map();
  for (const f of filings) {
    if (!byIssuer.has(f.issuer_cik)) byIssuer.set(f.issuer_cik, []);
    byIssuer.get(f.issuer_cik).push(f);
  }

  const clusters = [];
  for (const [cik, entries] of byIssuer) {
    const buyers = new Set(entries.map((e) => e.reporter_cik));
    if (buyers.size >= 3) {
      const total = entries.reduce((s, e) => s + Number(e.total_value), 0);
      clusters.push({
        ticker: entries[0].issuer_ticker,
        name: entries[0].issuer_name,
        buyerCount: buyers.size,
        totalDollars: total,
      });
    }
  }

  clusters.sort((a, b) => b.totalDollars - a.totalDollars);
  return clusters;
}

const clusters = await findClusters();
console.log(`Found ${clusters.length} clusters with 3+ buyers:`);
for (const c of clusters) {
  console.log(`  ${c.ticker.padEnd(6)} ${c.buyerCount} buyers  ${c.totalDollars.toLocaleString()}  ${c.name}`);
}

For the production-grade version with cron scheduling and Slack output, see the cluster buying guide.

Example 4: 13F holdings for a manager

// Berkshire Hathaway's manager CIK is 1067983
const url = new URL("https://api.edgarkit.com/v1/filings");
url.searchParams.set("form_type", "13F-HR");
url.searchParams.set("manager_cik", "1067983");
url.searchParams.set("limit", "1");

const resp = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.EDGARKIT_API_KEY}` },
});
const { data } = await resp.json();
const filing = data[0];

console.log(`Filing date: ${filing.filed_at}`);
console.log(`Holdings count: ${filing.holdings.length}`);
console.log();
console.log("Top 10 by value:");

const top = filing.holdings
  .sort((a, b) => Number(b.value_usd) - Number(a.value_usd))
  .slice(0, 10);

for (const h of top) {
  console.log(`  ${h.ticker.padEnd(6)} ${Number(h.value_usd).toLocaleString().padStart(15)}  ${h.issuer_name}`);
}

Each holding line includes the CUSIP, the resolved ticker, the issuer CIK, the value in USD, and the share count.

Example 5: Real-time 8-K webhooks with Express

Register the webhook:

const resp = await fetch("https://api.edgarkit.com/v1/webhooks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.EDGARKIT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://your-server.com/edgarkit-events",
    filters: {
      form_types: ["8-K"],
      items: ["5.02", "1.01", "2.01"],
    },
  }),
});
console.log(await resp.json());

Receive and verify in Express:

import express from "express";
import crypto from "node:crypto";

const app = express();
const WEBHOOK_SECRET = process.env.EDGARKIT_WEBHOOK_SECRET;

// Raw body required for signature verification
app.post(
  "/edgarkit-events",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-edgarkit-signature"] || "";
    const expected = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

    // Constant-time comparison to prevent timing attacks
    const sigBuf = Buffer.from(signature, "hex");
    const expBuf = Buffer.from(expected, "hex");
    if (
      sigBuf.length !== expBuf.length ||
      !crypto.timingSafeEqual(sigBuf, expBuf)
    ) {
      return res.status(401).send("invalid signature");
    }

    const filing = JSON.parse(req.body.toString());
    console.log(
      `New 8-K: ${filing.issuer_ticker} items=${JSON.stringify(filing.items)}`
    );
    res.status(204).end();
  }
);

app.listen(3000, () => console.log("Listening on :3000"));

Use raw body parsing on the webhook route specifically, JSON.parse before signature verification will fail because whitespace changes break the hash.

For the full webhook lifecycle including retries, idempotency, and verification edge cases, see the webhook guide.

Putting it together

A reusable Node.js client class:

// edgarkit.js
export class EdgarKit {
  constructor(apiKey = process.env.EDGARKIT_API_KEY, base = "https://api.edgarkit.com") {
    this.apiKey = apiKey;
    this.base = base;
  }

  async filings(params = {}) {
    const url = new URL(`${this.base}/v1/filings`);
    for (const [k, v] of Object.entries(params)) {
      if (v !== undefined && v !== null) url.searchParams.set(k, v);
    }
    const resp = await fetch(url, {
      headers: { Authorization: `Bearer ${this.apiKey}` },
    });
    if (!resp.ok) throw new Error(`EdgarKit ${resp.status}: ${await resp.text()}`);
    return (await resp.json()).data;
  }

  insiderPurchases({ ticker, minValue = 100000, limit = 50 } = {}) {
    return this.filings({
      form_type: 4,
      transaction_code: "P",
      ticker,
      min_value: minValue,
      limit,
    });
  }

  thirteenF({ managerCik, limit = 1 }) {
    return this.filings({ form_type: "13F-HR", manager_cik: managerCik, limit });
  }
}

// Usage:
// const ek = new EdgarKit();
// const buys = await ek.insiderPurchases({ ticker: "NVDA" });

FAQ

Do I need a Node SDK?

No. Native fetch is enough. The API is REST + JSON. A thin client class like the one above is the most you'll need.

What Node version do I need?

Node 18+ for native fetch. On older versions install node-fetch or use axios.

How do I handle rate limits in Node?

EdgarKit returns HTTP 429 with a Retry-After header. Wrap your fetch in a small retry function that reads the header and waits. The free tier is 10 requests per minute, Basic is 60, Pro is 300.

Do webhooks work with serverless functions?

Yes. Both Vercel and AWS Lambda can receive EdgarKit webhooks. The constraint is raw-body access for signature verification, which both platforms support with the right framework configuration.

TypeScript types?

The API responses are stable JSON. You can either generate types from the OpenAPI spec at api.edgarkit.com/v1/openapi.json, or hand-write a few interface types for the fields you use most.